WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 3 | 4 || 6 |

«Том Миллер Managed DirectX*9 Программирование графики и игр **омпэ*> Предисловие Боба Гейнса Менеджера проекта DirectX SDK корпорации Microsoft SAMS [Pi] KICK START Managed DirectX 9 ...»

-- [ Страница 5 ] --

private void UpdatelnputState!) { // Check the mouse state MouseState state = device.CurrentMouseState;

string mouseState = "Using CurrentMouseState: \r\n";

// Current location of the mouse mouseState += string. Format ("{0}х{1}х{2}\r\n", state.X, state.Y, state.Z);

// Buttons byte[] buttons = state.GetMouseButtons();

for(int i = 0;

i < buttons.Length;

i++) mouseState += string.Format("Button {0} (l}\r\n", i, buttons[i] != 0 ? "Pressed" : "Not Pressed");

textBoxl.Text = mouseState;

} Здесь, вместо использования вспомогательной функции для получе­ ния текущих данных, мы берем мгновенное состояние (мгновенный от­ печаток) мыши. Затем выводится текущая позиция мыши по осям X, Y, и Z (Ось Z обычно относится к колесу или скролу мыши), также как и со­ стояние кнопок. Запуская приложение теперь, мы обратим внимание, что большую часть времени, «позиция» мыши сообщается как 0x0x0, даже когда мы вращаем мышь вокруг своей оси. Почему же не обновляется пользова­ тельский интерфейс? В действительности, данные мыши сообщаются в относительных единицах оси (относительно последнего фрейма), а не в абсолютных. Если мы хотим сообщить о данных мыши в абсолютных значениях, необходимо сразу после создания устройства изменить стро­ ку «свойства» следующим образом:

device.Properties.AxisModeAbsolute = true;

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

Глава 15. Управление устройствами ввода Использование игровых клавиатур и джойстиков В то время как основные принципы использования джойстика, мыши и клавиатуры похожи (они совместно используют тот же самый класс устройств Device), имеются некоторые особенности, касающиеся при­ менения джойстиков, поскольку они в меньшей степени стандартизиро­ ваны. Мы можем хотя бы в какой-то мере предполагать, что у мыши бу­ дут две кнопки, а клавиатура будет иметь по крайней мере 36 клавиш. Формы, размеры и расположение кнопок и осей на джойстиках не пред­ сказуемы. Вы можете иметь один джойстик с двумя осями и двумя кноп­ ками, другой с 3 осями, 10 кнопками, третий подразумевает педаль и т. д. Не существует также и значения по умолчанию SystemGuid для созда­ ния джойстика. Мы должны вначале перечислить джойстики. Перепи­ шите функцию создания устройства, используя листинг 15.4.

Листинг 15.4. Инициализация джойстиков. public bool Initializelnput() { // Create our joystick device foreach(DeviceInstance di in Manager.GetDevices(DeviceClass.GameControl, EnumDevicesFlags.AttachedOnly)) { // Pick the first attached joystick we see device = new Device(di.InstanceGuid) ;

break;

} if (device == null) // We couldn't find a joystick return false;

device.SetDataFormat(DeviceDataFormat.Joystick) ;

device.SetCooperativeLevel(this, CooperativeLevelFlags.Background | CooperativeLevelFlags.NonExclusive);

device.Properties.AxisModeAbsolute = true;

device.Acquired();

while(running) { UpdatelnputState();

Application.DoEvents ();

} return true;

} Здесь мы используем метод перебора, рассмотренный в начале этой главы, для того, чтобы найти подсоединенный джойстик или игровую Часть IV. Звук и устройства ввода клавиатуру. Поскольку мы не создавали устройство с идентификатором GUID, необходимо сообщить Directlnput о типе устройства, используя метод SetDataFormat. Также необходимо возвратить соответствующее значение в случае, если указанное устройство отсутствует. Перепишем основную функцию, чтобы обработать этот вариант:

static void Main() { using (Forml frm = new Form1()) { frm.Show ();

if (!frm.Initializelnput()) MessageBox.Show("Couldn't find a joystick.");

} } Современные джойстики обычно имеют несколько осей управления, и диапазон этих осей, как правило, неизвестен. Мы должны обработать все эти оси в диапазоне 10,000 единиц. Вполне возможно, что устрой­ ство не поддерживает такое разрешение, но Directlnput будет эмулиро­ вать его. Нам необходимо найти оси на джойстике и переопределить ди­ апазон. Добавьте следующий код сразу после создания устройства:

// Enumerate any axes foreach(DeviceObjectInstance doi in device.Objects) { if ((doi.Objectld & (int)DeviceObjectTypeFlags.Axis) != 0) { // We found an axis, set the range to a max of 10,000 device.Properties.SetRange(ParameterHow.Byld, doi.Objectld, new InputRange(-5000, 5000));

} } Этим способом мы можем перечислить объекты любого устройства. Выполнение данного метода на клавиатуре, например, возвратит объект для каждой клавиши клавиатуры. В нашем случае определяем найден­ ные оси, затем изменяем свойства данного объекта (с учетом номера ID) таким образом, чтобы гарантировать соответствие указанным диапазо­ нам. И последнее, что мы должны сделать, — переписать пользовательс­ кий интерфейс:

private void UpdatelnputState() { // Check the joystick state Глава 15. Управление устройствами ввода JoystickState state = device.CurrentJoystickState;

string joyState = "Using JoystickState: \r\n";

joyState += string.Format("{0}x{l}", state.X, state.Y);

textBoxl.Text = joyState;

} Мы могли бы, используя предшествующее перечисление, обнаружить все элементы джойстика и соответственно изменить пользовательский интерфейс, но это не даст нам ничего нового. В данном примере, использующем метод UpdatelnputState, мы моди­ фицируем только X и Y оси, которые присутствуют в большинстве джой­ стиков. УПРАВЛЕНИЕ ДИАПАЗОНОМ Игровые клавиатуры (и цифровые джойстики) обычно перескаки­ вают от одного экстремального значения диапазона к другому. В примере, рассмотренном ранее, когда игровая клавиатура находи­ лась «в покое», ее значение было бы равно «0». Если бы вы нажали левую кнопку, ось X переместилась бы мгновенно в минимальное значение диапазона, то же самое и с правой кнопкой — резкое пе­ ремещение в максимальное значение без каких-либо промежуточ­ ных значений. Существует два типа джойстиков, цифровые и аналоговые. Цифро­ вой джойстик мы только что описали. В аналоговых джойстиках ди­ апазоны, как правило, встроены. Хороший пример этого — штурва­ лы управления полетом. В этих джойстиках имеется широкий диа­ пазон перемещений по осям, что позволяет достаточно четко от­ слеживать эти перемещения.

Устройства обратной связи Использование обратной связи в вашем приложении позволяет ощу­ тить наибольшее погружение в игровой процесс. Например, врезаясь в дерево на скорости 100 миль в час, при наличии джойстика с такой опци­ ей вы можете реально ощутить удар, передающийся в руку. Первое, что хотелось бы сразу отметить при упоминании эффектов обратной связи Force Feedback (дословно обратная связь по усилию), это то, что мы должны иметь эксклюзивный доступ к устройству. К счастью, для таких устройств используется установка по умолчанию режима экс­ клюзивного доступа и фонового режима. Итак, создадим устройство для этого примера (аналогично процедуре для джойстика). Перепишите метод Initializelnput, как в листинге 15.5.

Часть IV. Звук и устройства ввода Листинг 15.5. Инициализация устройства с обратной связью. private ArrayList effectList = new ArrayList();

public bool Initializelnput() { // Create our joystick device foreach(DeviceInstance di in Manager.GetDevices(DeviceClass.GameControl, EnumDevicesFlags.AttachedOnly | EnumDevicesFlags.ForceFeeback)) { // Pick the first attached joystick we see device = new Device(di.InstanceGuid);

break;

} if (device == null) // We couldn't find a joystick return false;

device.SetDataForaat(DeviceDataFormat.Joystick);

device.SetCooperativeLevel(this, CooperativeLevelFlags.Background | CooperativeLevelFlags.Exclusive);

device.Properties.AxisModeAbsolute = true;

device.Acquire ();

// Enumerate any axes foreach(DeviceObjectInstance doi in device.Objects) { if ((doi.ObjectId & (int)DeviceObjectTypeFlags.Axis) != 0) { // We found an axis, set the range to a max of 10,000 device.Properties.SetRange(ParameterHow.ById, doi.Objectld, new InputRange(-5000, 5000));

} } II Load our feedback file EffectList effects = null;

effects = device.GetEffects(@"..\..\idling.ffe", FileEffectsFlags.ModifylfNeeded);

foreach(FileEffect fe in effects) { EffectObject myEffect = new EffectObject(fe.EffectGuid, fe.EffectStruct, device);

myEffeet.Download();

effectList.Add(myEffect);

} while(running) { UpdatelnputState();

Application.DoEvents ();

} return true;

} Глава 15. Управление устройствами ввода Как вы видите, здесь имеется несколько изменений. Мы установили эксклюзивный доступ и переопределили метод перебора returnforce для устройства обратной связи. Кроме того, мы установили еще раз диапазо­ ны и определили эффекты обратной связи. Следует отметить, что мы можем создавать эффект обратной связи из файла. В этом случае каждый эффект может быть представлен в виде массива отдельных эффектов обратной связи. Код, включенный в CD диск (и в нашем тексте) будет использовать файл idling.ffe, поставляемый с DirectX SDK. Также на CD диске вы можете найти копию исходника. После того как мы загрузили список эффектов из файла, мы про­ сматриваем каждый отдельный эффект обратной связи и создаем объект эффекта. Далее мы загружаем этот эффект в фактическое устройство таким образом, чтобы устройство могло использовать его и проверять его состояние, и наконец добавляем этот эффект к локальному списку массива. Теперь, когда мы имеем набор загруженных эффектов, мы можем ис­ пользовать их в нашем устройстве. Для этого необходимо добавить ме­ тод, который определяет, проигрывается ли данный эффект в настоящее время или нет, и если нет — запустить его. Это будет гарантировать про­ игрывание эффекта при запущенном приложении.

private void PlayEffects () { // See if our effects are playing. foreach(EffectObject myEffect in effectList) { if (ImyEffect.EffectStatus.Playing) { // If not, play them myEffect.Start (1, EffectStartFlags.NoDownload);

} } } Целесообразно также добавить вызов PlayEffects в наш метод UpdateInputState. ИСПОЛЬЗОВАНИЕ РЕДАКТОРА УСИЛИЯ Force Editor Утилита Force Editor, которая поставляется с DirectX SDK, может ис­ пользоваться для создания любого эффекта обратной связи по же­ ланию. Помимо этого возможно создавать и редактировать эти эф­ фекты вручную в нашем коде, затем сохранять их в файл и исполь­ зовать в дальнейшем.

Часть IV. Звук и устройства ввода Краткие выводы В этой главе мы рассмотрели. Управление с клавиатуры. Управление с помощью мыши. Управление с помощью джойстика и игровой клавиатуры. Устройства обратной связи Force Feedback. В последующих главах мы рассмотрим возможности Direct3D для двухмерной графики, а также добавление сетевых возможностей.

ЧАСТЬ V 2D ГРАФИКА Глава 16. Приложение Direct3D для 2D-графики Глава 17. Использование DirectDraw для рендеринга 2D-графики Часть V. 2D графика Глава 16. Приложение Direct3D для 20-графики В этой главе мы рассмотрим вопросы программирования и рендерин­ га 20-графики с помощью Direct3D. Мы коснемся следующих тем. • Создание полноэкранного устройства. • Использование класса Sprite. • Рендеринг спрайтов.

Создание полноэкранного устройства отображения Зачастую рендеринг громоздких трехмерных сцен «сложен» по опре-" делению. Несмотря на то, что большинство современных компьютерных игр используют богатую трехмерную графику, существует множество игровых сюжетов, где нет необходимости использовать «причуды» 3Dграфики. Поэтому имеет смысл создание таких игр в 2D-исполнении. Учитывая то, что Direct3D может отображать достаточно сложные трехмерные сцены, надо полагать, что рендеринг 2D-объектов не будет для нас чем-то чрезмерно громоздким. Создание полноэкранного устройства не слишком отличается от со­ здания устройств, работающих в оконном режиме, за исключением неко­ торых особенностей. Создайте форму, которая будет использоваться нами в дальнейшем при визуализации 20-графики. Как обычно, перед созданием устройства не­ обходимо добавить директиву using, объявить переменную устройства и установить стиль окон. Как только проект создан, необходимо объявить две константы, опре­ деляющие полноэкранный размер (в нашем случае 800x600):

public const int ScreenWidth = 800;

public const int ScreenHeight = 600;

Эти константы определяют ширину и высоту полноэкранного окна, которое мы будем использовать для отображения. Теперь мы напишем метод инициализации полноэкранного устройства, приведенный в лис­ тинге 16.1.

Листинг 16.1. Инициализация полноэкранного устройства public void InitializeGraphics () { Глава 16. Приложение Direct3D для 2D-графики // Set our presentation parameters PresentParameters presentParams = new PresentParameters() ;

presentParams.SwapEffeet = SwapEffeet.Discard;

' // Start up full screen Format current = Manager.Adapters[0].CurrentDisplayMode.Format;

if (Manager.CheckDeviceType(0, DeviceType.Hardware, current, current, false)) { // Perfect, this is valid presentParams.Windowed = false;

presentParams.BackBufferFormat = current;

presentParams.BackBufferCount = 1;

presentParams.BackBufferWidth = ScreenWidth;

presentParams.BackBufferHeight = ScreenHeight;

} else { presentParams.Windowed = true;

} // Create our device device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

} Начало этого метода должно быть нам знакомо. Поскольку в данном разделе мы рассматриваем 2D-объекты, нет никаких причин объявлять буфер глубины в приведенном методе инициализации. Можно предположить, что текущий формат режима дисплея для за­ данного по умолчанию адаптера является подходящим для создаваемого полноэкранного устройства. Для того чтобы принять это предположе­ ние, целесообразно вызвать метод проверки CheckDeviceType. Если этот тип устройства поддерживается, измените параметры описания, чтобы ' определить полноэкранное устройство. Параметр Windowed устанавливается в значение «false», так как уст­ ройство будет работать в полноэкранном режиме. Для отображения в полноэкранном режиме мы будем применять вторичный буфер, поэтому ширина и высота вторичного буфера должны быть установлены в соот­ ветствии с заявленными константами размера экрана. ОБРАБОТКА СОБЫТИЙ ОТКАЗОВ ПРИ СОЗДАНИИ УСТРОЙСТВА Предположим (хотя это маловероятно), что при создании устрой­ ства произойдет отказ и, несмотря на то, что формат устройства перед созданием был проверен, вторичный буфер не будет создан.

Часть V. 2D графика Формат 800x600, выбранный для нашего примера, является доста­ точно распространенным, и есть все основания полагать, что дан­ ный формат будет поддерживаться. Вопросы определения подхо­ дящего режима дисплея рассматривались в главе 2. ИСПОЛЬЗОВАНИЕ ПОЛНОЭКРАННОГО УСТРОЙСТВА Полноэкранный режим не является особенностью отображения 2Dграфики. Используя подход, рассмотренный в листинге 16.1, мож­ но создать практически любое, в том числе и трехмерное, полноэк­ ранное устройство. В предыдущих главах, рассматривая объекты ЗD-графики, мы опирались в основном на оконный режим по при­ чине большой трудоемкости в отладке кода для полноэкранного режима. В случае отказа при создании устройства можно всегда вернуться к оконному режиму, управляя соответствующим значением в структуре параметров представления. После заполнения данной структуры мы мо­ жем приступить к созданию устройства. Итак, мы создаем полноэкранное устройство. Вполне естественно, что у нас возникает желание предусмотреть простую процедуру завершения работы приложения. Для этого запишем известный нам по прошлым гла­ вам обработчик события нажатия клавиши, добавив его к нашему коду:

protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Escape) // Quit this. Closed;

base.OnKeyUp (e);

} Рендеринг спрайтов Приложение Direct3D уже содержит объекты текстурирования или текстуры, которые можно уподобить спрайтам. Однако, визуализация текстур подразумевает некий примитив или поверхность, на которую данная текстура должна отображаться. Класс Sprite позволяет выполнять эту процедуру автономно и независимо от использования примитивов. Добавьте следующие переменные для нашего приложения:

private Texture spriteTexture;

private Sprite sprite;

private Rectangle textureSize;

Глава 16. Приложение Direct3D для 2D-графики Эти переменные позволяют сохранить спрайт и текстуру, которая бу­ дет использоваться для рендеринга спрайта. Для правильного выполне­ ния данной процедуры необходимо знать размер сохраняемой текстуры (переменная textureSize). Таким образом, необходимо инициализировать эти переменные, до­ бавив следующий код в конце метода InitializeGraphics, сразу после со­ здания устройства:

// Create our texture spriteTexture = TextureLoader.FromFile(device, @"..\..\logo.tga") ;

using (Surface s = spriteTexture.GetSurfaceLevel(0)) { SurfaceDescription desc = s.Description;

textureSize = new Rectangle(0, 0, desc.Width, desc.Height);

} sprite = new Sprite(device);

Файл logo.tga находится на CD диске. После создания текстуры необ­ ходимо определить ее размер. Мы находим поверхность, на которую будет отображаться текстура, и, используя ее описание, устанавливаем размер текстуры. Затем мы со­ здаем пример класса Sprite. Класс Sprite позволяет, используя различные текстуры, рисовать лю­ бое количество спрайтов. Для этого, вместо того чтобы копировать наш класс Sprite, мы создадим новый класс, позволяющий обработать множе­ ственные спрайты. Добавьте класс, приведенный в листинге 16.2.

Листинг 16.2. Добавление класса Sprite public class GraphicsSprite { // static data for our sprites private static readonly Vector3 Center = new Vector3(0, 0, 0);

private static readonly Random rnd = new Random();

// Instance data for our sprites private Vector3 position;

private float xUpdate = 1.4f;

private float yUpdate = 1.4f;

public GraphicsSprite(int posx, int posy) { position = new Vector3(posx,posy,1);

xUpdate += (float)rnd.NextDoublef);

yUpdate += (float)rnd.NextDoublef);

}.

Часть V. 2 D графика public void Draw(Sprite sprite, Texture t, Rectangle r) ( sprite.Draw(t, r, Center, position, Color.White);

} } При желании мы можем вращать спрайты (даже если приложение не предусматривает эту операцию). Когда мы рисуем спрайт (sprite.Draw), одним из параметров этой фун­ кции, используемым для задания вращения, является центр спрайта. Про­ ще сохранить это значение здесь, вместо того чтобы каждый раз при вы­ зове функции создавать новый вектор. В данной процедуре мы также добавляем движению спрайтов элемент случайности (параметр Random). Для каждого спрайта устанавливаются параметры, отвечающие за те­ кущую позицию спрайта и его скорость перемещения. Различные значения скорости по координатам X и Y позволяют спрайту перемещаться не только под углом в 45°. Объект имеет заданные началь­ ные значения скорости по осям X и Y, которые затем изменяются случай­ ным образом. Параметр цвета в данном методе используется для придания оттенка отображаемой текстуре. Белый цвет (как в данном примере) позволит отобразить текстуру с естественным оттенком, а использование красного добавит текстуре крас­ ный оттенок. Используя общий класс Sprite, можно переписать код и включить про­ рисовку спрайтов. Перед этим необходимо добавить новую переменную для записи ото­ бражаемых спрайтов:

System.Collections.ArrayList ar = new System.Collections.ArrayList();

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

// Add a few sprites ar.Add(new GraphicsSprite(0, 0));

ar.Add(new GraphicsSprite(64,128)) ;

ar.Addfnew GraphicsSprite(128,64)) ;

ar.Addfnew GraphicsSprite(128,128)) ;

ar.Addfnew GraphicsSprite(192,128)) ;

ar.Addfnew GraphicsSprite(128,192));

ar.Addfnew GraphicsSprite(256,256));

Глава 16. Приложение Direct3D для 2D-графики По желанию можно изменять количество и местоположение добавля­ емых спрайтов. Имея коллекцию спрайтов, мы можем попробовать отобразить их на экране. Для этого в процедуре OnPaint добавьте после вызова BeginScene сле­ дующий код рендеринга спрайтов:

// Begin drawing our sprites with alpha blend sprite.Begin(SpriteFlags.None);

// Draw each sprite in our list. foreach(GraphicsSprite gs in ar) gs.Draw(sprite, spriteTexture, textureSize);

// Notify Direct3D we are done drawing sprites sprite.End() ;

Функция Begin начинает процедуру рендеринга, затем для каждого спрайта вызывается метод Draw. Метод заканчивается обязательным опе­ ратором End. Теперь мы можем запустить приложение. Увы, мы видим, что наши спрайты не перемещаются, несмотря на то, что мы задали скорость перемещения. Это очевидно, поскольку в нашей программе не задан метод обновле­ ния спрайтов. Нечто подобное мы уже проходили в первых главах, когда пытались задать вращение элементарному треугольнику. Добавьте в класс GraphicsSprite метод Update, приведенный в листин­ ге 16.3.

Листинг 16.3. Обновление спрайтов. public void Update(Rectangle textureSize) { // Update the current position position.X += xUpdate;

position.Y += yUpdate;

// See if we've gone beyond the screen if (position.X > (Forml.ScreenWidth - textureSize.Width)) { xUpdate *= - 1 ;

} if (position.Y > (Forml.ScreenHeight - textureSize.Height)) { yUpdate *= -1;

} // See if we're too high or too the left if (position.X < 0) { xUpdate *= -1;

) if (position.Y < 0) { yUpdate *= -1;

Часть V. 2D графика } В рассмотренном методе изменение положения спрайта определяется двумя отдельными переменными скорости. Затем проверяется местоположение спрайта относительно границ эк­ рана, и, если спрайт коснулся границы экрана, направление перемеще­ ния изменяется на противоположное, и объект как бы отражается от края экрана. Для вызова этого метода добавьте следующий код в начале процеду­ ры OnPaint:

// Before we render each sprite, make // sure we update them foreach(GraphicsSprite gs in ar) gs.Update(textureSize);

Функция Update обращается к каждому из имеющихся спрайтов. Те­ перь, запустив приложение, мы увидим объекты, перемещающиеся на экране в различных направлениях. ИСПОЛНЕНИЕ СПРАЙТОВ С ПАРАМЕТРОМ ПРОЗРАЧНОСТИ ЦВЕТА «АЛЬФА» При использовании текстуры, хранящейся на CD диске, можно за­ метить белую прозрачную окантовку текстуры. Эта текстура содер­ жит параметр «альфа» — прозрачность объекта. Использование данного параметра несложно: вместо sprite.Begin(SpriteFlags.None);

напишите:

sprite.Begin(SpriteFlags.AlphaBlend);

На рис. 16.1 показан результат визуализации текстуры, использу­ ющей параметр прозрачности «альфа».

Глава 16. Приложение Dircct3D для 20-графики Рис. 16.1. Перемещающиеся спрайты Анимация спрайтов В основном, при анимации 2D-cnpaйтов отображаются не просто ста­ тические картинки, а скорее, последовательности изображений. В прин­ ципе, основной механизм анимации спрайтов такой же, как и для стати­ ческих изображений. Потгому в качестве отправной точки мы возьмем копию программы, написанной для перемещения статических объектов. Главные различия между данным приложением и предыдущим зак­ лючаются во вспомогательном классе, используемом для рендеринга спрайтов. Процедура инициализации осталась практически той же са­ мой, только в исходнике кода поменялось имя файла «sprites.tga», чтобы подчеркнуть то, что он включает в себя множественные спрайты. Так как вместо одного статичного мы будем отображать на экране большое коли­ чество небольших анимированных объектов, необходимо случайным образом задать им первоначальное положение. Таким образом, перепи­ шем конструктор для класса Sprite следующим образом: public GraphicsSprite() ( position = new Vector3(rnd.Hext(Forml.ScreenWidth-SpriteSize), rnd.Hext(Forml.ScreenHeight-SpriteSize), 1);

xUpdate += (float) rnd.UextDouble();

yUpdate += (float)rnd.UextDouble(), column = rnd.Next(NumberSpritesCol);

row = rnd.Next(NumberSpritesRow);

if ((column % 3) == 0) xUpdate * =. - l ;

if ((row % 2) == 0) yUpdate *= - 1 ;

} Часть V. 2D графика Позиция спрайта на экране задается следующим образом. Определя­ ется разность между шириной и высотой экрана (ScreenWidth и ScreenHeight) и размером спрайта по ширине и высоте соответственно, В результате функции случайных чисел возвращаются целые числа, лежа­ щие в пределах от 0 до этих значений. Однако, мы до сих пор не задали размер спрайта. Размер рамки используемого в нашем примере спрайта составляет 50x45. Теперь мы можем установить эти константы:

private const int SpriteSizeWidth = 50;

private const int SpriteSizeHeight = 45;

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

private const int NumberSpritesRow = 6;

private const int NumberSpritesCol = 5;

Также необходимо объявить переменные, которые будут использовать­ ся для определения текущего фрейма:

private int column = 0;

private int row = 0;

Теперь, когда мы изменили конструктор класса Sprite, необходимо переписать последние строки в методе инициализации, чтобы добавить наши спрайты:

// Add a few sprites for (int i = 0;

i < 100;

i++) ar.Add (new GraphicsSprite() ) ;

Число спрайтов можно при желании изменять. Теперь, зная размеры спрайта, мы должны изменить метод обновле­ ния Update, см. листинг 16.4.

Глава 16. Приложение Direct3D для 2Б-графики Листинг 16.4. Обновление анимированных спрайтов. public void Updated() { // Update the current position position.X += xUpdate;

position.Y += yUpdate;

// See if we've gone beyond the screen if (position.X > (Forml.ScreenWidth - SpriteSizeWidth)) { xUpdate *= -1;

} if (position.Y > (Forml.ScreenHeight - SpriteSizeHeight) { yUpdate *= -1 :

// See if we're too high or too the left if (position.X < 0) { xUpdate *= -1;

} if (position.Y < 0) { yUpdate *= -1;

) II Now update the column column++;

if (column >= NumberSpritesCol) { row++;

column = 0;

} if (row >= NumberSpritesRow) { row = 0;

} } Начало этого метода такое же, как и в предыдущем варианте, только теперь мы используем размер спрайта вместо входящего параметра раз­ мера текстуры. Далее выполняется последовательный перебор спрайтов по столб­ цам и строкам и возврат в начало после прохождения всего набора спрай­ тов. Теперь, когда мы изменили метод обновления, можно убрать пара­ метр из обращения к методу Update:

318 foreach(GraphicsSprite gs in ar) gs. Updated;

Часть V. 2D графика Последнее, что мы должны сделать, изменить метод рисования Draw и, соответственно, оформить вызов этой процедуры. Перепишите метод Draw следующим образом: public void Draw(Sprite sprite, Texture t) { sprite.Draw(t, new Rectangle(column * SpriteSizeWidth, row * SpriteSizeHeight, SpriteSizeWidth, SpriteSizeHeight), Center, position, Color.White);

} Легко заметить, что в списке параметров вместо размера появился полигон спрайта, рассчитанный с помощью сохраненных данных объек­ та анимации. Теперь мы можем переписать процедуру OnPaint для вызо­ ва метода Draw: foreach(3raphicsSprite gs in ar) gs.Draw(sprite, spriteTexture);

Запустив приложение, мы увидим на экране движущиеся анимированные спрайты, как на рис. 16.2.

Рис. 16.2. Анимированные спрайты Глава 16. Приложение DirectD для 2D-графики Краткие выводы В данной главе мы рассмотрели. • Использование полноэкранного режима. • Рендеринг спрайтов. • Рендеринг анимированных спрайтов. В следующей главе мы рассмотрим использование в этих же целях DirectDraw вместо Direct3D.

Часть V. 2D графика Глава 17. Использование DirectDraw для рендеринга 2D-графики Одним из недостатков использования Direct3D для 2D-приложений являются аппаратные ограничения. Direct3D более сложен, чем DirectDraw, и требует большего количества ресурсов. В связи с этим в некоторых системах целесообразно использовать устройство DirectDraw. Более того, это может касаться и современных графических плат. При имеющихся аппаратных ограничениях DirectDraw может решить пробле­ му, связанную с ресурсами системы. В этой главе мы рассмотрим использование DirectDraw для 2D-rpaфики, включая. Использование полноэкранного режима. • Рендеринг спрайтов. • Рендеринг анимированных спрайтов.

Создание полноэкранного устройства DirectDraw Прежде, чем создать устройство DirectDraw, необходимо выполнить следующие действия: 1. Создать новый проект. 2. Добавить ссылки на Microsoft.DirectX.dll и Microsoft.DirectDraw.dll. 3. Включить новую директиву using для Microsoft.DirectX.DirectDraw в главном файле кода. 4. Установить стиль окна как Opaque (непрозрачный) и AllPaintinglnWmPaint, подобно тому, как это делалось для ЗD-приложений. 5. Добавить индивидуальную переменную для DirectDraw устройства. Практически все эти пункты мы выполняли в наших ЗD-приложениях, различия могут быть только в добавляемых ссылках. Следует отметить, что в DirectDraw для отображения 2D-объектов вместо текстур используются поверхности (surfaces). По сути, поверх­ ность — это сохраненные данные полигонального изображения. При ото­ бражении отдельного спрайта в полноэкранном устройстве нам понадо­ бятся три поверхности, которые мы добавляем к приложению: private Surface primary = null;

private Surface backBuffer = null;

private Surface sprite = null;

Очевидно, что поверхность sprite является объектом рендеринга со­ храненного изображения. Мы могли бы отображать нашу поверхность, Глава 17. Использование DirectDraw для рендеринга 20-графики привязываясь непосредственно к экрану, но это может привести к разры­ ву изображений при отображении нескольких сцен. По этой причине в DirectDraw необходимо выполнять всю процедуру рендеринга во вторич­ ном буфере с последующим копированием в первичный (если вспомнить, в Direct3D данные действия выполнялись бы автоматически). Прежде чем начать, требуется написать процедуру обработки нажа­ тия клавиши выхода «Escape». Добавим обработчик этого события в наше полноэкранное приложение:

protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Escape) // Quit this.Closed ;

base.OnKeyUp (e);

} Теперь, как обычно записываем метод инициализации, см. лис­ тинг 17.1.

Листинг 17.1. Инициализация DirectDraw устройства. public void InitializeGraphics() { SurfaceDescription description = new SurfaceDescription();

device = new Device() ;

// Set the cooperative level. device.SetCooperativeLevel(this, CooperativeLevelFlags.FullscreenExclusive);

// Set the display mode width and height, and 16 bit color depth. device.SetDisplayMode(ScreenWidth, ScreenHeight, 16, 0, false);

// Make this a complex flippable primary surface with one backbuffer description.SurfaceCaps.PrimarySurface = description.SurfaceCaps.Flip = description.SurfaceCaps.Complex = true;

description.BackBufferCount = 1;

// Create the primary surface primary = new Surface(description, device);

SurfaceCaps caps = new SurfaceCaps!);

caps.BackBuffer = true;

// Get the backbuffer from the primary surface backBuffer = primary.GetAttachedSurface(caps) ;

// Create the sprite bitmap surface, sprite = new Surfaced"..\..\logo.bmp", new SurfaceDescriptionO, device);

// Set the colorkey to the bitmap surface.

11 Зак. Часть V. 2D графика // which is what the CoiorKey struct is initialized to. ColorKey ck = new CoiorKey();

sprite. SetColorKey (ColorKeyFlags.SourceDraw, ck);

} Как вы можете видеть, этот метод несколько более сложен, чем вариант для Direct3D. После создания устройства DirectDraw мы вызываем проце­ дуру установки уровней доступа к различным ресурсам платы SetCooperativeLevel. Поскольку для DirectDraw мы используем полноэкранное при­ ложение, нет никакой необходимости в совместном использовании ресур­ сов (устанавливаем эксклюзивный доступ FullScreenExclusive). Далее устанавливается режим отображения, первые два параметра — ширина и высота экранного окна. Третий параметр определяет насыщен­ ность цвета, в нашем случае используется 16-разрядный формат. Исполь-зование 32-разрядного параметра цвета позволяет формировать более богатые цвета, но и требует при этом большего количества ресурсов. Чет­ вертый параметр — частота обновления монитора, для установки по умол­ чанию используется значение «О». Последний параметр применяется ред­ ко, он определяет использование стандартного режима VGA. После создания устройства и определения режимов отображения фор­ мируются поверхности. Вначале создается первичная поверхность, точ­ нее, ее описание. В нашем случае описание первичной поверхности вклю­ чает в себя опции PrimarySurface, Flip и Complex. Поверхность со свой­ ством Complex может иметь другие присоединенные поверхности, и в нашем случае прикрепленная поверхность будет являться содержимым вторичного буфера. Теперь, имея соответствующее описание, создается первичная повер­ хность. Вызывая метод GetAttachedSurface, мы можем определить или задать поверхность вторичного буфера на базе первичной поверхности. И последним действием в этой процедуре является создание поверх­ ности спрайта. Код, записанный на CD диске, использует для этой цели файл dx5 logo.bmp. Поскольку поверхность создается из файла, нам не понадобится дополнительная информация об этой поверхности (поле описания поверхности останется пустым). ПАРАМЕТР ПРОЗРАЧНОСТИ В DIRECTDRAW DirectDraw не поддерживает параметр прозрачности цвета «альфа». В варианте нашего приложения для Direct3D мы просто использо­ вали соответствующую технику для придания фону нашего спрайта свойства прозрачности. В DirectDraw для этого используется так на­ зываемый ключевой цвет (color key), который делает один из цве­ тов прозрачным. Правда такая схема имеет и недостатки, одним из Глава 17. Использование DirectDraw для рендеринга 20-графики которых является невозможность использования этого цвета в изоб­ ражении спрайта. В нашем случае изображение имеет черный цвет фона, который и бу­ дет использоваться в качестве ключевого (для этого значение ключевого цвета устанавливается нулевым). Необходимо отметить, что в процедуре инициализации имеется еще несколько констант, которые мы не объявили:

public const int ScreenWidth = 800;

public const int ScreenHeight = 600;

private static readonly Rectangle SpriteSize = new Rectangle(0,0,256,256);

Ширина и высота экрана встречались и раньше. Поскольку размер изображения, загружаемого из файла и используемого в качестве спрай­ та, известен, проще его сохранить сейчас, чем определять в дальнейшем. Размер изображения можно также определить, используя описание по­ верхности (параметры Width и Height). Теперь мы должны добавить вызов процедуры инициализации из на­ шего основного метода:

static void Main() { using (Forml frm = new Forml()) ( // Show our form and initialize our graphics engine frm.Show();

frm.InitiaiizeGraphics();

Application.Run(frm);

Для обработки данных спрайта в нашем приложении нам понадобит­ ся общий класс:

public class GraphicsSprite ( // static data for our sprites private static readonly Random rnd = new Random!);

// Instance data for our sprites private int xPosition = 0;

private int yPosition = 0;

private float xUpdate = 1.4f;

private float yUpdate = 1.4 f;

///

/// Constructor for our sprite /// /// Initial x position /// Initial у position public GraphicsSprite(int posx, int posy) Часть V. 2D графика { xPosition = yPosition = xUpdate += yUpdate += } } posx;

posy;

(float)rnd.NextDouble ();

(float)rnd.NextDouble();

В этом случае класс сохраняет текущую позицию левого верхнего угла спрайта. Обратите внимание на то, что вместо используемого в Direct3D параметра Vector3 здесь сохраняются два отдельных целочисленных зна­ чения. Целочисленные значения здесь вполне корректны и предпочтитель­ ны, поскольку DirectDraw использует экранные координаты устройства. Третий параметр (параметр глубины) опускается, поскольку в прило­ жении DirectDraw он не используется. Указанный класс будет сохранять скорость для каждого направления перемещаемых по экрану спрайтов, используя начальное значение и слу­ чайное приращение скорости спрайта. Теперь мы должны переписать наш класс Sprite, добавив процедуры обновления и рисования, см. листинг 17.2.

Листинг 17.2. Обновление и рисование спрайтов. public void Draw(Surface backBuffer, Surface spriteSurface, Rectangle spriteSize) { backBuffer.DrawFast(xPosition, yPosition, spriteSurface, spriteSize, DrawFastFlags.DoNotWait | DrawFastFlags.SourceColorKey);

} public void Update(Rectangle spriteSize) { // Update the current position xPosition += (int)xUpdate;

yPosition += (int)yUpdate;

// See if we've gone beyond the screen if (xPosition > (Forml.ScreenWidth - spriteSize.Width)) { xUpdate *= - 1 ;

Глава 17. Использование DirectDraw для рендеринга 2D-графики if (yPosition > (Forml.ScreenHeight - spriteSize.Height)) { yUpdate *= -1;

} // See if we're too high or too the left if (xPosition < 0) ^ { xUpdate *= -1;

} if (yPosition < 0) { yUpdate *= -1;

} } РАЗЛИЧИЯ МЕЖДУ ВЫЗОВАМИ DRAW И DRAWFAST Вызов процедуры рисования вызывает поверхность из вторичного При использовании аппаратной акселерации (что имеет место в буфера, куда будет отображаться спрайт, а также поверхность спрайта и большинстве современных графических плат) между методами Draw ее размер. Процедура DrawFast просто рисует спрайт в указанном поло­ и DrawFast нет никакого различия. Однако, в случае программной жении, используя весь спрайт в качестве исходного материала. Исполь­ реализации метод DrawFast приблизительно на 10 % быстрее, чем зуемые флажки сообщают DirectDraw, что он «не должен ожидать» окон­ метод Draw, правда, ценой надежности и устойчивости. Метод Draw чания процедуры рисования, и что для включения параметра прозрачно­ является более гибким и позволяет реализовать различные опера­ сти необходимо использовать цветовой ключ (который был установлен ции по отношению к отображаемым спрайтам. Для простых же опе­ при создании спрайта). раций рисования целесообразно использовать метод DrawFast. Приведенный в листинге 17.2 метод Update для DirectDraw идентичен аналогичному методу для Direct3D. Позиция и скорость перемещения спрайта связаны между собой, а направление перемещения изменяется на обратное в случае приближения спрайта к краю экрана. Теперь, пол­ ностью описав класс Sprite, мы должны обработать все имеющиеся в нашем приложении спрайты. Будем использовать тот же самый метод, который использовался для Direct3D. Добавьте следующую переменную:

Часть V. 2D графика System.Collections.ArrayList ar = new System.Collections.ArrayList();

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

// Add a few sprites ar.Add(new GraphicsSprite(0,0) );

ar.Add(new GraphicsSprite (64,128));

ar.Addfnew GraphicsSprite(128, 64));

ar.Add(new GraphicsSprite(128,128)) ;

ar.Addfnew GraphicsSprite(192,128)) ;

ar.Add(new GraphicsSprite(128,192));

ar.Addfnew GraphicsSprite(256,256)) ;

Осталось лишь добавить процедуру визуализации. Механизм ренде­ ринга остался прежним, поэтому мы можем добавить следующую пере­ грузку в класс windows form:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { Microsoft.DirectX.DirectXException.IgnoreExceptions();

foreach(GraphicsSprite gs in ar) gs.Update(SpriteSize);

backBuffer.ColorFill(O);

foreach(GraphicsSprite gs in ar) gs.Draw(backBuffer, sprite, SpriteSize);

primary.Flip(backBuffer, FlipFlags.DoNotWait);

this.Invalidated ;

} Первое, что бросается в глаза в этом методе, — выключение парамет­ ра обработки исключительных ситуаций в Управляемом DirectX. Этот момент мы обсудим позже, сейчас же следует отметить, что необходимо пользоваться этим с достаточной осторожностью. Теперь, когда мы, используя сохраненный размер, изменили каждый спрайт, очищаем вторичный буфер (в нашем случае для этого служит за­ полнение буфера цветом). Для ЗО-приложения аналогичную функцию выполнял метод Clear. Затем каждый спрайт коллекции отображается во вторичный буфер, после чего мы можем обновить экран. Для этого вызывается метод Flip, копирующий данные из вторичного буфера в первичный. При запуске приложения мы должны увидеть результат, схожий с вы­ полнением приложения Direct3D.

Глава 17. Использование DirectDraw для рендеринга 20-графики ИСПОЛЬЗУЕМЫЕ ИСКЛЮЧЕНИЯ В данном приложении могут произойти два вида исключительных ситуаций. Первая (WasStillDrawingException) случится при попытке копирования первичной поверхности или рисунка во вторичный бу­ фер в момент времени, когда система еще не завершила предыду­ щую операцию draw. В Direct3D этот сценарий будет попросту про­ игнорирован, если вы используете флажок SwapEffect.Discard. Вторая возможная ситуация — InvalidRectangleException. В Direct3D, если спрайты вышли за пределы экрана, это не так принципиально, поскольку устройство работает в мировых координатах, которые являются, по сути, бесконечными. В DirectDraw это является важ­ ным, так как мы не сможем выполнять процедуру рендеринга спрай­ та, вышедшего за пределы экрана.

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

private private private private const const const const int int int int NumberSpritesRow = 6;

NumberSpritesCol = 5;

SpriteSizeWidth = 50;

SpriteSizeHeight = 45;

Исходный текст программы, включенной в CD диск, использует файл sprites.bmp. ИСПОЛЬЗОВАНИЕ НЕКВАДРАТНЫХ ПОВЕРХНОСТЕЙ В Direct3D текстуры не должны быть обязательно прямоугольными, но обязаны иметь в качестве длины стороны число, являющееся сте­ пенью двойки. Большинство современных плат способны поддер­ живать нестандартные текстуры, не удовлетворяющие данному ог­ раничению. В предыдущей главе для анимации мы использовали спрайт, «рас­ ширенный» до прямоугольника с соответствующей длиной сторон, для того чтобы такая текстура поддерживалась большинством плат. DirectDraw не использует для создаваемых поверхностей подобные правила, поэтому в нашем примере спрайт имеет размеры, соот­ ветствующие изображению.

Часть V. 2D графика Мы должны сохранить также положение объекта анимации, которое является комбинацией строки и столбца. Добавьте следующие перемен­ ные к классу Sprite: private int column = 0;

private int row = 0;

Теперь мы попробуем переписать конструктор для размещения на эк­ ране случайного изображения спрайта беспорядочным образом: public GraphicsSprite() { xPosition = rnd.Next(Forml.ScreenWidth-SpriteSizeWidth);

yPosition = rnd.Next(Forml.ScreenHeight-SpriteSizeHeight);

xUpdate += (float)rnd.NextDouble ();

yUpdate += (float)rnd.NextDouble();

column = rnd.Next(NumberSpritesCol);

row = rnd.Next(NumberSpritesRow);

if ((column \ 3) == 0) xUpdate *= -1;

if ((row% 2) == 0) yUpdate *= -1;

} Так же понадобиться переписать метод Draw: public void Draw(Surface backBuffer, Surface spriteSurface) { backBuffer.DrawFast(xPosition, yPosition, spriteSurface, new Rectangle(column * SpriteSizeWidth, row * SpriteSizeHeight, SpriteSizeWidth, SpriteSizeHeight), DrawFastFlags.DoNotWait | DrawFastFlags.SourceColorKey);

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

Листинг 17.3. Обновление анимированных спрайтов public void Updated { // Update the current position xPosition+= (int)xUpdate;

yPosition += (int)yUpdate;

Глава 17. Использование DirectDraw для рендеринга 20-графики // See if we've gone beyond the screen if (xPosition > (Forml.ScreenWidth - SpriteSizeWidth)) { xUpdate *= -1;

} if (yPosition > (Forml.ScreenHeight - SpriteSizeHeight)) { yUpdate *= -1;

} // See if we're too high or too the left if (xPosition < 0) { xUpdate *= -1;

} if (yPosition < 0) { yUpdate *= -1;

} // Now update the column column++;

if (column >= NumberSpritesCol) { row++;

column = 0;

} if (row >= NumberSpritesRow) { row = 0;

} После процедуры обновления основная программа должна выдать несколько ошибок при трансляции. Чтобы избежать этого, перепишите код создания спрайта:

// Add a few sprites for(int i = 0;

i < 100;

i++) ar. Add (new GraphicsSprite() ) ;

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

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { DirectXException.IgnoreExceptions ();

foreach(GraphicsSprite gs in ar) gs. Updated;

backBuffer.ColorFill(O);

foreach(GraphicsSprite gs in ar) gs.Draw(backBuffer, sprite);

primary.Flip(backBuffer, FlipFlags.DoNotWait);

this. Invalidated;

} Часть V. 2D графика жгт mi т и п г Таким образом, мы твидим, тттч"Ч различия между Direct3D и DirectDraw что « п п т г т » т т т » и а ч т у т не столь значительные, как можно было предположить вначале. ' I 'ni^Fii • л й и л п л ! » Краткие выводы В данной главе мы рассмотрели следующие вопросы. Использование полноэкранного режима. Рендеринг спрайтов. Рендеринг анимированных спрайтов. В следующей главе мы рассмотрим добавление сетевых возможнос­ тей, в частности, работу одноранговых сетей.

ЧАСТЬ VI ДОБАВЛЕНИЕ СЕТЕВЫХ ВОЗМОЖНОСТЕЙ Глава 18. Организация сети с равноправными узлами с помощью DirectPlay Глава 19. Создание сессии Client/Server Глава 20. Особенности более совершенного использования сетей Глава 21. Методы достижения максимального быстродействия Часть VI. Добавление сетевых возможностей Глава 18. Организация сети с равноправными узлами с помощью DirectPlay В этой главе мы рассмотрим работу DirectPlay для сетей с архитекту­ рой "peer-to-peen", включая. Адреса DirectPlay. • Равноправные объекты. • Модель события DirectPlay. Пересылка данных другим участникам сети.

Адреса DirectPlay Весьма заманчивым может показаться участие в игре одновремен­ но нескольких игроков. На сегодняшний день имеется целый класс весьма популярных игр, которые вообще не имеют варианта для од­ ного игрока. Управляемый DirectX включает DirectPlay API, который может ис­ пользоваться для осуществления сетевых возможностей. Прежде чем начать писать код, необходимо проверить добавление соответствую­ щих директив и ссылок для DirectPlay, в частности, ссылки на Microsoft.DirectX.DirectPlay. Мы полагаем, что все это уже сделано, и не будем останавливаться на этом еще раз. Если у вас имеется компьютер, подсоединенный к локальной сети или сети Internet, это означает, что он уже имеет свой адрес протокола TCP/ IP, который отличает его от других машин в сети. Мы должны, используя DirectPlay, присвоить каждому компьютеру свой адрес. Процедура создания индивидуального адреса в DirectPlay выглядит достаточно просто:

Address address = new Address();

address. ServiceProvider = Address.ServiceProviderTcpIp;

Здесь мы указываем, что хотим использовать для DirectPlay протокол TCP/IP, который является на сегодняшний день наиболее распространен­ ным. Ниже приводятся еще несколько протоколов и соединений, доступ­ ных для DirectPlay. • Протокол TCP/IP. Протокол IPX. • Технология BlueTooth. • Соединения с использованием последовательных портов. Соединения модемов напрямую.

Глава 18. Организация сети с равноправными узлами с помощью DirectPlay 333 В дальнейшем, при написании приложений DirectPlay мы будем иметь дело только с TCP/IP протоколом. Но это вовсе не означает, что мы не можем использовать другие протоколы, поэтому записанный код может быть применим и к другим перечисленным выше протоколам и соедине­ ниям. -"^ ИСПОЛЬЗОВАНИЕ «URL» СТРОКИ В КАЧЕСТВЕ АДРЕСА Адреса могут быть также определены в форме указателя URL, как и все Web-адреса, например, http://www.mycompany.com. Первая секция URL указывает на тип протокола (в данном случае протокол http). Web-страницы, которые мы обычно видим, имеют формат язы­ ка HTML. Ниже приводится пример использования указателя URL для задания адреса в DirectPlay: x-directplay:/provider=%7BEBFE7BA0-628D-HD2AE0F-006097B014H%7D;

hostname= www.mygameserver.com;

port=9798 Обратите внимание на тип этого URL— x-directplay. Остальная часть адреса определяет идентификатор провайдера, имя хоста и порт. ИСПОЛЬЗОВАНИЕ TCP/IP АДРЕСА Имеется четыре конструктора для адресного объекта, большинство из которых имеет дело с TCP/IP протоколом. Используемый нами ранее и не имеющий параметров конструктор не устанавливает про­ токолы автоматически. Другие три конструктора задействуют про­ токол TCP/IP автоматически, рассмотрим их подробнее: public Address ( System.String hostname, System.Int32 port ) public Address ( System.Net.IPAddress address ) public Address ( System.Net.IPEndPoint address ) Каждый из этих конструкторов выполняет одну и ту же операцию, только с различными наборами данных. Первый создает адрес TCP/IP, устанавливает имя хоста и соответствующий порт. Имя хоста может быть именем компьютера, адресом Internet (например, www.mycompany.com) или IP-адресом (например, 192.168.2.1). Два других варианта могут принимать существующие адреса TCP/IP и конвертировать их в соответствующий адрес DirectPlay. До сих пор мы не использовали такие термины, как «имя хоста» и «порт». Попробуем с помощью адресного объекта написать проце Часть VI. Добавление сетевых возможностей дуру добавления этих компонентов к нашему адресу. Каждый ком­ понент имеет название (значение в формате string) и ассоцииро­ ванные данные (либо строчные, либо в виде массива). Да и сам класс Address содержит стандартные ключевые имена для создаваемых адресов. Запишем код, позволяющий вручную присвоить имя хос­ та и порт существующему адресу:

Address address = new Address));

address.ServiceProvider = Address.ServiceProviderTcpIp;

address.AddComponent(Address.KeyHostname, "www.mygameserver.com");

address.AddComponent(Address.KeyPort, 9798);

В действительности, конструкторы класса Address, имеющие вход­ ные параметры, выполняют эти действия достаточно точно. Помимо этого существует несколько заданных по умолчанию клю­ чевых имен, которые могут использоваться в различных случаях (на­ пример, KeyPhoneNumber для прямых соединений модем-модем), кроме того, пользователь может создавать и свои имена. Компонент «hostname» особенно важен, когда мы связываемся с другим компьютером. При этом DirectPlay достаточно гибок, что­ бы распознать имя хоста на вашей машине, и в этом случае нет необходимости использовать указанные ключи для локальных ад­ ресов.

Создание Р2Р-соединения Теперь мы можем перейти к созданию сети. Следует отметить, что DirectPlay поддерживает системы и с архитектурой «peer-to-peen> (рав­ ный с равным), и с архитектурой «клиент-сервер». В этой главе мы обсу­ дим соединения с архитектурой «peer-to-peer», которые в дальнейшем будем называть пиринговыми или Р2Р-соединениями. Архитектура Р2Р подразумевает, что каждый участник соединения связан непосредственно с каждым другим участником соединения. Для небольшого числа узлов этот метод работает достаточно надежно. Име­ ется множество популярных на сегодняшний день примеров примене­ ния Р2Р-сетей. Многие из систем совместного использования файлов (Kazaa, Napster и т. д.) представляют собой именно Р2Р-сети. Прежде чем начать работу в DirectPlay с Р2Р-соединением, необходи­ мо уточнить несколько вопросов: например, выбрать тип соединения и определить, будет'ли наша машина поддерживать работу сети или при­ соединится к уже существующему сеансу. Основным классом, который мы будем использовать для нашего со­ единения, будет класс Peer. Для начала объявим переменные соединения и адреса для этого класса:

Глава 18. Организация сети с равноправными узлами с помощью DirectPlay 335 private Peer connection = null;

private Address deviceAddress = null;

Далее создадим в верхнем правом углу наше формы две кнопки. Пер­ вая кнопка «Host» будет использоваться для организации собственной сессии, а вторая «Connect» — для подключения к существующему сеан­ су. Добавьте там же третью кнопку «Send Data», которая будет управлять передачей данных в сети (до соединения эта кнопка должна быть забло­ кирована). Затем в нижней части экрана создадим экран индикации со­ стояния сети и приступим к работе с сетью. Вначале мы запишем функцию инициализации объекта Peer и устано­ вим любое заданное по умолчанию состояние, см. листинг 18.1.

Листинг 18.1. Инициализация объекта Peer. private void InitializeDirectPlay(bool host) { // Create our peer object connection = new Peer() ;

// Check to see if we can create a TCP/IP connection if { (!IsServiceProviderValid(Address.ServiceProviderTcpIp)) // Nope, can't, quit this application MessageBox.Show("Could not create a TCP/IP service provider.", "Exiting", MessageBoxButtons.OK, MessageBoxIcon.Information) ;

this. Close();

} // Create a new address for our local machine deviceAddress = new Address();

deviceAddress.ServiceProvider = Address.ServiceProviderTcpIp;

} При создании объекта Peer мы использовали конструктор, задающий свойства объекта по умолчанию. Существует и другой вариант, который имеет в качестве параметров флажки инициализации, приведенные в таб­ лице 18.1. Таблица. 18.1. Флажки объекта Флажок DisableParamaterValidation Описание Данный флажок отключает проверку параметров этого соединения, позволяя получить выигрыш в быстродействии 336 Флажок DisableLinkTuning Часть VI. Добавление сетевых возможностей Описание Отключает возможность настройки скорости передачи данных в DirectPlay, автоматически устанавливается максимальная скорость Открывает расширенное окно для сетевых игр Позволяет установить все опции по умолчанию HintLanSession None Эти опции могут быть весьма полезны, однако сейчас они нам не по­ надобятся, поэтому мы будем использовать для этого приложения конст­ руктор, устанавливающий свойства по умолчанию. Так как для работы приложения нам потребуется служба поддержки протокола TCP/IP, мы должны проверить ее наличие в нашей системе. Для этого можно ис­ пользовать метод IsServiceProviderValid:

private bool IsServiceProviderValid(Guid provider) I // Ask DirectPlay for the service provider list ServiceProviderlnformation[] providers = connection.GetServiceProviders(true);

// For each service provider in the returned list... foreach (ServiceProviderlnformation info in providers) f // Compare the current provider against the passed provider if (info.Guid == provider) return true;

} // Not found return false;

} Эта процедура определяет все имеющиеся в нашей системе службы поддержки протоколов и возвращает информацию обо всех обнаружен­ ных в системе службах, в том числе и тех, которые на данный момент недоступны (при значении входного параметра функции равного «true»). Если в возвращаемом массиве имеется идентификатор GUID, это гово­ рит о том, что служба поддержки обнаружена, и мы можем приступить к созданию адреса. В случае если протокол TCP/IP не поддерживается, приложение завершит работу. Мы также должны предусмотреть возмож­ ность освобождения объекта Peer после завершения соединения, для этого имеется перегрузка по умолчанию Dispose: if (connection != null) connection.Dispose();

Теперь мы должны записать код для каждого сценария.

Глава 18. Организация сети с равноправными узлами с помощью DirectPlay Начало сеанса Итак, в нашей форме имеются две кнопки, соответственно, для сеан­ са Host и для сеанса Connect. Мы должны исключить возможность нажа­ тия одновременно двух кнопок или нажатия одной из них во время сеан­ са. Соответствующие обработчики событий приведены ниже:

private void buttonl_Click(object sender, System.EventArgs e) { buttonl.Enabled = false;

button2.Enabled = false;

InitializeDirectPlay(true);

} private void button2_Click(object sender, System.EventArgs e) { buttonl.Enabled = false;

button2.Enabled = false;

InitializeDirectPlay(false);

} Как вы можете видеть, разница между ними состоит только в конеч­ ном параметре, передающемся в метод InitializeDirectPlay (значение па­ раметра «true» для установки компьютера главным, другое значение — «false» — для присоединяемой машины). Мы могли бы упростить дан­ ную процедуру, сделав ее общей для обеих кнопок:

private void button_Click(object sender, System.EventArgs e) { buttonl.Enabled = false;

button2.Enabled = false;

InitializeDirectPlay((sender == buttonl));

} Результат выполнения этой процедуры такой же, но при этом мы из­ бегаем дублирования кода. Если нажатой окажется первая кнопка, метод InitializeDirectPlay будет вызываться с входным параметром, имеющим значением «true», в противном случае, со значением «false». Теперь необходимо написать метод, распознающий и сообщающий приложению о выбранном сеансе подключения. DirectPlay включает структуру описания приложения, которая содержит всю необходимую информацию. Нижеприведенный код для этой структуры необходимо добавить в конец метода InitializeDirectPlay:

// Set up an application description ApplicationDescription desc = new ApplicationDescription();

desc.SessionName = "MDXBookPeerSession";

1 2 Зак Часть VI. Добавление сетевых возможностей desc.GuidApplication = new Guid(41039, 1702,1503,178, 101, 32, 13, 121, 230, 109, 59);

Наиболее важный параметр этой структуры — GuidApplication. Иден­ тификатор GUID однозначно определяет наше приложение, и таким об­ разом, все примеры данного приложения должны его использовать. Дру­ гой параметр — имя сеанса (SessionName) — необходим при выполне­ нии нескольких сеансов связи. В таблице 18.2 приведены остальные па­ раметры структуры ApplicationDescription. Таблица 18.2. Параметры структуры ApplicationDescription Параметр GuidApplication Guidlnstance Описание Уникальный идентификатор приложения Уникальный идентификатор соединения, который генерируется DirectPlay. Идентифицирует отдельные экземпляры приложения Максимальное число пользователей в данном соединении. Нулевое значение (по умолчанию) устанавливает неограниченное число игроков Число подсоединенных на данный момент пользователей Определяют поведение соединения, могут использоваться по отдельности или в различных комбинациях: • ClientServer • FastSigned • FullSigned • MigrateHost • NoDpnServer • NoEnumerations • RequirePassword Имя соединения, определенное пользователем Пароль для установления соединения. Это значение должно иметь пустой указатель (null) в случае, если флажок RequirePassword не был установлен MaxPlayers CurrentPlayers Flags SessionName Password Теперь, имея структуру описания, мы можем приступить к созданию соединения. Добавьте следующий код в конец метода инициализации: if (host) { try { // Host a new session Глава 18. Организация сети с равноправными узлами с помощью DirectPlay connection.Host(desc, deviceAddress);

// We can assume after the host call succeeds we are hosting AddText("Currently Hosting a session.");

EnableSendDataButton(true);

} catch { AddText("Hosting this session has failed.");

} } Как вы можете видеть, вызов режима Host оказался в достаточной мере «простым», входными параметрами этой функции являются описание соединения и локальный адрес. Здесь мы использовали самый простой из восьми существующих вариантов загрузки. Остальные также исполь­ зуют в качестве входных параметров указанное описание и один или бо­ лее адресных объектов. В качестве параметра может использоваться объект, содержащий ин­ формацию об игроке (мы кратко опишем это чуть позже), или флажок, определяющий поведение соединения, например, позволяющий DirectPlay открыть диалоговое окно для получения недостающей информации об адресах. ПАРОЛИ НЕ ЗАШИФРОВАНЫ Будьте внимательны при передаче паролей. Если вы хотите защитить пароли, необходимо перед пересылкой зашифровать их вручную. Итак, далее мы помещаем вызов процедуры Host в блок кода, позво­ ляющий отследить событие, когда метод по каким-либо причинам не выполняется. Мы могли бы отслеживать только те исключительные си­ туации, которые входят в класс DirectPlayExcetion, но для нашего про­ стого примера мы попросту рассмотрим все подобные случаи. Но снача­ ла создадим процедуры, позволяющие модифицировать пользовательс­ кий интерфейс:

private void AddText(string text) f labell.Text += (text + "\r\n");

} private void EnableSendDataButton(bool enable) { button3.Enabled = enable;

Часть VI. Добавление сетевых возможностей Нет необходимости останавливаться на этих методах подробно, по­ скольку они весьма очевидны. Запуск приложения и нажатие кнопки «Host» приведет к установке Р2Р-соединения, отобразит на экране за­ пись о текущем состоянии и сделает доступным использование третьей кнопки «Send Data». Теперь нам необходим еще один узел для установки сеанса соединения. Добавим следующую процедуру в конец метода ини­ циализации:

else { try { connection.FindHosts(desc, null, deviceAddress, null, 0, 0, 0, FindHostsFlags.OkToQueryForAddressing);

AddText("Looking for sessions.");

} catch { AddText("Enumeration of sessions has failed.");

} ) Мы снова включаем процедуру в блок try/catch, чтобы отследить воз­ можные ошибки, а также обновляем наше сообщение пользователю. Од­ нако, здесь вместо метода Connect мы вызываем метод FindHosts. В неко­ торых случаях возможно непосредственное присоединение к имеюще­ муся сеансу, но в большинстве случаев вначале необходимо найти суще­ ствующие хосты. Аналогично методу Host, метод FindHosts принимает описание соеди­ нения и локальный адрес устройства. Вторым параметром является ад­ рес главного компьютера (пока мы его не знаем, поэтому используем пу­ стой указатель). Четвертый параметр этого метода — любые данные о приложении, которые мы хотим передать на сервер. Следующие три параметра определяют режим работы хостов: коли­ чество попыток передачи пакетов, ожидание перед следующей переда­ чей данных и время таймаута. Задавая нулевые значения этих парамет­ ров, мы определяем установку для них режима по умолчанию. После­ дний параметр может иметь одно или больше значений из списка FindHostsFlags, который приведен в таблице 18.3. Таблица 18.3. Значения и флажки структуры FindHostFlags Параметр None Описание Устанавливает все значения по умолчанию Глава 18. Организация сети с равноправными узлами с помощью DirectPlay 341 Параметр NoBroadcastFallback Описание Отключает режим ретрансляции данных на серве­ ре. Поддержку возможности передачи данных службой поддержки можно проверить с помощью метода GetSpCaps класса Peer Позволяет устройству DirectPlay отображать диалоговые окна для уточнения текущей информации По умолчанию выполнение процедуры заканчива­ ется немедленно. При использовании данного флажка процедура будет функционировать до тех пор, пока не завершатся все попытки соединения OkToQueryForAddressing Sync Запустив теперь приложение, обратите внимание, что нажатие на кноп­ ку «Connect» вызовет диалоговое окно, запрашивающее адрес удален­ ной машины. Вы можете оставить это поле пустым, чтобы отследить со­ стояние всей сети. Вы можете также определить имя или IP-адрес иско­ мой удаленной машины. Следует отметить, что метод FindHosts не воз­ вращает никаких сведений относительно найденных хостов.

Использование модели событий Большинство действий в DirectPlay выполняются асинхронно. Резуль­ тат выполнения процедуры возвращается немедленно, и сразу после это­ го DirectPlay начинает отработку поставленных задач. На данном этапе нам понадобится обработчик ситуации, когда DirectPlay закончил выпол­ нение операции или выдал сообщение о состоянии приложения. В нашем предыдущем вызове метода FindHosts выполнялся поиск всех возможных соединений. Допустим, хост был найден. Поскольку нам не­ обходимо об этом знать, мы должны зафиксировать данное событие (FindHostResponsee). Добавьте следующую строку сразу после создания объекта Peer в методе InitializeDirectPlay: connection.FindHostResponse += new FindHostResponseEventHandler(OnFindHost);

Мы также должны создать обработчик этого события, см. листинг 18.2. Листинг 18.2. Обработчик события обнаружения «Found Host». private void OnFindHost(object sender, FindHostResponseEventArgs e) { lock(this) { Часть VI. Добавление сетевых возможностей / / D o nothing if we're connected already if (connected) return;

connected = true;

string foundSession = string.Format ("Found session ((О}), trying to connect.", e.Message.ApplicationDescription.SessionName);

this.Beginlnvoke(new AddTextCallback(AddText), new object[] { foundSession });

// Connect to the first one ((Peer)sender).Connect(e.Message.ApplicationDescription, e.Message.AddressSender, e.Message.AddressDevice, null, ConnectFlags.OkToQueryForAddressing);

} } Как вы можете видеть, данный обработчик возвращает объект FindHostsReponseEventArgs, в котором содержится информация о най­ денном хосте. Вполне возможно, что могут быть одновременно найдены два хоста или соединения. Наличие двух процессов, оперирующих од­ ним набором данных, может привести к возникновению больших про­ блем, и нам важно избежать данной ситуации. Мы будем использовать для блокировки одного из потоков ключевое слово языка С# lock (кото­ рое по сути является аналогом вызова комбинации Monitor.Enter Monitor.Exit в блоке try/finally). Таким образом, при использовании пер­ вого потока второй поток блокируется. В итоге мы сможем присоединиться только к одному хосту, поэтому мы должны объявить логическую переменную, определяющую наличие уже установленного соединения. Если соединение существует, мы мо­ жем вернуться из процедуры, в противном случае переменной присваи­ вается значение «false», чтобы поиск соединения продолжался дальше. Теперь нам необходимо сообщить пользователю, что сеанс обнару­ жен, и мы готовы начать соединение. Для этого в программе использует­ ся метод Beginlnvoke, вызывающий так называемый делегат с набором параметров, определенным управляющим узлом. Данный метод работа­ ет асинхронно (классический вариант для DirectPlay). Вызвав Beginlnvoke, мы можем продолжать работу нашего приложения, а делегат будет вы­ полнятся параллельно. Имеется также метод Invoke, который принимает те же самые пара­ метры, но выполняется синхронно. Определение для делегата имеет вид:

private delegate void AddTextCallback(string text);

Таким образом, мы создали делегат для вызова уже существующего метода AddText. Данные действия позволяют не блокировать поток при Глава 18. Организация сети с равноправными узлами с помощью DirectPlay 343 отображении диалогового окна, как можно быстрее получить необходи­ мые нам данные и завершить процедуру. Наконец, мы готовы начать соединение (вернемся к рассмотрению листинга 18.2). Вызов подключения Connect имеет четыре модели заг­ рузки, и, как обычно, мы используем самую простую из них. Обратите внимание, что для передачи в процедуру Connect мы используем пара­ метры описания, принятые от обработчика FindHostsEventArgs. Последние два пересылаемых нами параметра определены нашим приложением. Первый из них — любые определяемые пользователем данные, которые мы хотим передать в вызов Connect. Второй позволяет DirectPlay сделать запрос на получение адресной информации, хотя в этом нет особой необходимости, поскольку мы уже имеем ее. Для того чтобы обеспечить синхронное выполнение, необходимо задействовать флажок Sync, напомним, что по умолчанию установлен обратный вари­ ант. Другие модели метода Connect содержат один или несколько парамет­ ров, которые мы не используем в нашем примере. Одним из выходных параметров может являться асинхронный обработчик вызова (в DirectPlay все асинхронные обращения могут возвращать подобный параметр), ко­ торый при необходимости можно использовать, чтобы отменить опера­ цию с помощью функции CancelAsyncOperation. Но здесь нам это не по­ надобится. Другие параметры являются определяемыми пользователем контекстными переменными, которые могут использоваться для записи специфических данных. АДРЕСНАЯ ИНФОРМАЦИЯ При попытке присоединиться к полноэкранному сетевому прило­ жению желательно, чтобы любая информация о возможных сете­ вых соединениях появлялась внутри пользовательского интерфей­ са игры, а не в отдельном диалоговом окне. При разработке сетевых уровней для полноэкранного приложения вам вряд ли захочется или, более того, понадобится пересылать флажки OkToQueryForAddressing в методы Connect, Host или FindHost, поскольку при этом DirectPlay отобразит свое собствен­ ное диалоговое окно, которое, скорее всего, не будет соответство­ вать пользовательскому интерфейсу вашего приложения. Если вы используете указанные методы без этого флажка, и во время их выполнения возникает исключительная ситуация, впол­ не вероятно, что был пропущен какой-либо компонент адреса. Для варианта с протоколом TCP/IP это могло бы быть имя хоста (или сервера) или используемый порт. Предварительно необхо­ димо убедиться в наличии необходимых адресов и их компонен­ тов.

Часть VI. Добавление сетевых возможностей Работа в сети Теперь мы могли бы записать алгоритм обработчика, отслеживающе­ го завершение метода Connect. Для начала добавьте следующие строки к методу InitializeDirectPlay: connection.ConnectComplete += new ConnectCompleteEventHandler(OnConnectComplete);

Здесь же мы можем добавить и сам обработчик: private void OnConnectComplete(object sender, ConnectCompleteEventArgs e) ( // Check to see if we connected properly if (e.Message.ResultCode == ResultCode.Success) ( this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( "Connect Success." });

connected = true;

this.Beginlnvoke(new EnableCallback(EnableSendDataButton), new object[] ( true ( );

else ( this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( string.Format("Connect Failure: (0|", e.Message.ResultCode) lbconnected = false;

this.Beginlnvoke(new EnableCallback(EnableSendDataButton), new object[] ( false ( );

) Таким образом, мы получаем информацию об успешном или неудач­ ном соединении. В случае удачного соединения мы можем задействовать кнопку для передачи данных. В противном случае мы наоборот блокиру­ ем кнопку для передачи данных и присваиваем переменной наличия со­ единения значение «false», и тогда обработчик FindHosts продолжит от­ слеживать другие возможные соединения. Обратите внимание, что нам необходим делегат, управляющий режимом использования кнопки для передачи данных. Мы можем объявить его следующим образом: private delegate void EnableCallback(bool enable);

Теперь попробуем запустить два примера приложения и соединить их друг с другом. В первом примере нажимаем кнопку «Host», при этом дол­ жно появиться сообщение о том^что это приложение будет являться хос Глава 18. Организация сети с равноправными узлами с помощью DirectPlay 345 том соединения. Далее нажимаем кнопку «Connect» во втором примере, и после запроса об удаленном хосте мы должны увидеть результат со­ единения. Попытаемся переслать некоторые данные с помощью этого соедине­ ния. Для этого необходимо добавить соответствующий обработчик для кнопки передачи данных:

private void button3_Click(object sender, System.EventArgs e) { NetworkPacket packet = new NetworkPacket();

packet.Write(byte.MaxValue);

connection.SendTo((int)PlayerlD.AllPlayers, packet, 0, SendFlags.Guaranteed);

} При пересылке данных по сети, как правило, используется сетевой пакет, в который мы можем записывать любую информацию определен­ ного формата, за исключением ссылок на данный метод. Для этого при­ ложения нам не важен тип данных, поэтому для простоты мы будем пе­ редавать число в виде отдельного байта. Как мы видим, вызов метода Send весьма прост. Первый параметр PlayerlD — идентификационный номер игрока (или группы игроков, это мы обсудим позже), которому мы хотим послать данные. В данном примере мы посылаем данные всем игрокам. Вторым параметром является собственно сам сетевой пакет. Третий параметр — значение таймаута для передачи это­ го пакета. Нулевое значение этого параметра приведет к продолжению по­ пыток передачи пакета до того, как соединение прервется. И последний па­ раметр в этом вызове — один или несколько флажков структуры SendFlags. Примеры этих флажков и их описание приведены в таблице 18.4. Таблица. 18.4. Флажки структуры SendFlags Параметр Sync NoCopy Описание Устанавливает синхронные операции вместо заданных по умолчанию асинхронных Данный флажок выполняется только для перегрузок, принимающих в качестве сетевого пакета структуру GCHANDLE, позволяет DirectPlay использовать данные в GCHANDLE непосредственно, без создания отдельной копии. Данный флажок не может использо­ ваться с флажком NoComplete Использование этого флажка позволяет не отслеживать событие SendComplete. He используется с флажками NoCopy или Guaranteed NoComplete 346 Параметр CompleteOnProcess Описание Часть VI. Добавление сетевых возможностей Использование этого флажка позволяет осуществить проверку соединения и отследить событие передачи данных SendComplete. Используется с флажком Guaranteed Посылает сообщение для проверки наличия соединения Посылает сообщение с высоким приоритетом. Не может использоваться совместно с флажком PriorityLow Посылает сообщение с низким приоритетом. Не может использоваться совместно с флажком PriorityHigh По умолчанию DirectPlay определяет тот же самый порядок приема данных, что и при передаче. Если сообщения достигают компьютера в другом порядке, они будут буферизированы и переупорядочены. При установке данного флажка данные не переупорядочи­ ваются Задание этого флажка позволяет не отслеживать событие приема данных при пересылке их другому игроку или группе, в которой вы находитесь Использование этого флажка позволяет DirectPlay объединять пересылаемые пакеты Guaranteed PriorityHigh PriorityLow Nonsequential NoLoopBack Coalesce Теперь, когда мы, нажав соответствующую кнопку, отослали данные, нам понадобится процедура, отслеживающая момент приема данных: connection.Receive += new ReceiveEventHandler(OnDataReceive);

Here is the code for the actual event handler: private void OnDataReceive(object sender, ReceiveEventArgs e) { // We received some data, update our UI string newtext = string.Format ("Received message from DPlay Userld: 0x{0}", e.Message.SenderlD.ToString("x"));

this.Beginlnvoke(new AddTextCallback(AddText), new object[] { newtext });

} Метод достаточно прост, мы регистрируем событие приема данных (ReceiveEventArgs) и уведомляем об этом пользователя. Попробуем запустить два примера приложения (не важно, на одной или на двух машинах) и соединить их друг с другом. Как и раньше, у нас име Глава 18. Организация сети с равноправными узлами с помощью DirectPlay 347 ется и главное, и подключаемое приложения. Нажатие кнопки «Send Data» вызовет появление сообщений пользователю на обеих машинах. Это про­ изойдет автоматически, поскольку мы не использовали флажок NoLoopBack при вызове метода SendTo, и данные были переданы на всем участникам. Теперь, не завершая приложения, отключим соединение на главном компьютере и нажмем кнопку «Send Data» на втором экземпляре. Оче­ видно, что возникла ситуация, которая не может быть обработана, и при­ ложение «рухнуло». Рассмотрим этот вариант подробнее.

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

connection.SessionTerminated += new SessionTerminatedEventHandler(OnSessionTerminate);

Разрыв связи (событие Session.Terminated) возможен по различным причинам, например: отсоединение кабеля, выход из сеанса и т. д. Код для обработчика подобной ситуации имеет следующий вид:

private void OnSessionTerminate(object sender, SessionTerminatedEventArgs e) f // The session was terminated, close our peer object, re-enable our buttons this.Beginlnvoke(new DisconnectCallback(OnDisconnect), null);

} Итак, у нас имеются новый делегат и новый метод, которые должны быть вначале объявлены:

private delegate void DisconnectCallbackO;

private void OnDisconnect() { // Re-enable the UI buttonl.Enabled = true;

button2.Enabled = true;

button3.Enabled = false;

// Notify the user AddText("The host has quit, you have been disconnected.");

// Dispose of our connection, and set it to null connection.Disposed;

connection = null;

} Часть VI. Добавление сетевых возможностей Теперь понятно, почему мы выбрали такой подход. После разрыва соединения мы сначала устанавливаем состояние кнопок в исходное по­ ложение (по умолчанию), чтобы подготовиться к новому сеансу подклю­ чения. Затем обновляется пользовательский интерфейс и, наконец, по­ скольку старое соединение уже не имеет силы, устанавливается нулевое значение соединения. ОБРАБОТКА ПЕРЕДАЧИ ХОСТА Весьма заманчивой кажется передача управления после выхода хоста из сеанса любому из имеющихся узлов сети. В DirectPlay име­ ется возможность передачи управления сеансом, которая автома­ тически выберет новый хост из оставшихся машин. В разделе кон­ струирования нашей формы между кнопками «Connect» и «Send" Data» добавьте кнопку «Host Migration». Теперь, чтобы «оживить» данную опцию, мы должны переписать не­ сколько процедур в нашем приложении. В начале мы должны опре­ делить соответствующий флажок передачи перед вызовом Host:

// Should we allow host migration? desc.Flags = checkBoxl.Checked ? SessionFlags.MigrateHost : 0;

Как видите, если возможность этого действия проверена, включа­ ется флажок MigrateHost. Также мы должны включить управление этой кнопкой в методе OnDisconnect и в процедуре обработки на­ жатия кнопки (по аналогии с кнопками «Host» и «Connect»). Наконец, было бы неплохо обновить пользовательский интерфейс при выходе главного компьютера из соединения и передаче функ­ ций хоста следующей машине. Добавим необходимый для этого код:

connection.HostMigrated += new HostMigratedEventHandler(OnHostMigrate);

Обработчик этого события имеет следующий вид:

private void OnHostMigrate(object sender, HostMigratedEventArgs e) ( // The host has been migrated to a new peer this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( "The host was migrated." });

} Если бы наше приложение имело специфические опции хоста, мы могли бы задействовать их в этом методе, проверив значение па­ раметра NewHostld в объекте HostMigratedEventArgs.

Глава 18. Организация сети с равноправными узлами с помощью DirectPlay Краткие выводы В этой главе мы рассмотрели принципы использования DirectPlay для Р2Р-сетей, включая. • Адреса DirectPlay. • Равноправные объекты. • Модель события DirectPlay. • Пересылка данных другим участникам сети. • Передача функций хоста. В следующей главе мы рассмотрим работу в режиме мультиигрока с использованием клиент-серверной топологии.

Часть VI. Добавление сетевых возможностей Глава 19. Создание сессии Client/Server Р2Р-сети работают достаточно хорошо, когда мы имеем дело с неболь­ шим числом пользователей. Однако, когда речь заходит о сотнях, а то и тысячах игроков или пользователей, необходимо наличие выделенного сервера. При работе Р2Р-сети все передаваемые данные могли быть про­ дублированы каждому подключенному объекту. Для небольших групп эта операция вполне приемлема, но если в сети участвуют 10000 машин, это может надолго «подвесить» выполнение операций. В DirectPlay имеется два других сетевых класса, подобных рассмот­ ренному нами объекту Peer: это класс Client и класс Server, соответствен­ но клиент и сервер. В этой главе мы с помощью DirectPlay создадим при-. ложение клиент-сервер и рассмотрим следующие вопросы. Создание выделенных серверов. • Соединение с этими серверами, используя клиентский интерфейс. • Отслеживание подключения и отключения игрока. Передача игровых данных.

Создание выделенного сервера При создании приложения клиент-сервер, в первую очередь создает­ ся сервер. Большинство серверных приложений используют порты Internet. Например, Web-серверы практически всегда используют 80-й порт, а серверы F T P — 21-й, причем с открытым портом одновременно может работать только одно приложение. Наша задача — определить ин­ дивидуальный порт, с которым будет работать наше приложение. Первая тысяча номеров считается зарезервированной, хотя мы можем свободно использовать любой доступный порт. Для нашего приложения мы выберем порт 9798. Мы пока не будем использовать для нашего сервера какие-либо опции, только определим его пользовательский интерфейс и простейшие действия. Как обычно, мы должны создать нашу форму, добавить необходимые ссылки и директивы для DirectPlay и описать диалоговое окно. Подобно тому, как мы объявляли объект Peer, объявим объект Server:

private Server connection = null;

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

Глава 19. Создание сессии Client/Server Лисияг 19.1. Инициализация Сервера.

public void InitializeServerO { // Create our server object connection = new Server!);

// Check to see if we can create a TCP/IP connection if (!IsServiceProviderValid(Address.ServiceProviderTcpIp)) { // Nope, can't, quit this application MessageBox.Show("Could not create a TCP/IP service provider.", "Exiting", MessageBoxButtons.OK, MessageBoxIcon.Information);

this.Close();

} // Create a new address for our local machine Address deviceAddress = new Address();

deviceAddress.ServiceProvider = Address.ServiceProviderTcpIp;

deviceAddress.AddComponent(Address.KeyPort, SharedCode.DataPort);

// Set up an application description ApplicationDescription desc = new ApplicationDescription();

desc.SessionName = "MDX Book Server Session";

desc.GuidApplication = SharedCode.ApplicationGuid;

desc.Flags = SessionFlags.ClientServer | SessionFlags.NoDpnServer;

try { // Host a new session connection.Host(desc, deviceAddress) ;

// We can assume after the host call succeeds we are hosting AddText("Currently Hosting a session.");

} catch { AddText("Hosting this session has failed.");

} } Данный метод подобен методу инициализации объекта Peer. Методы IsServiceProviderValid и AddText были объявлены в предыдущей главе, поэтому здесь мы не станем на них останавливаться. Глядя на метод ини­ циализации сервера, можно обнаружить несколько важных отличий его создания от создания Р2Р-сети. Здесь мы добавили к адресу компонент порта (константа Shared­ Code.DataPort). Поскольку указанная константа требуется при создании и сервера, и клиента, необходимо создать отдельный фрагмент кода ShareCode, который будет использоваться обеими процедурами:

352 public class SharedCode { Часть VI. Добавление сетевых возможностей public static readonly Guid ApplicationGuid = n w Guid e (15741039, 1702,1503,178, 101, 32, 13, 121, 230, 109, 59);

public const int DataPort = 9798;

) Каждая из двух определенных нами констант понадобится нам в обе­ их процедурах создания объектов. Для Р2Р-сети весь выполняемый код формировался в одной программе, и не было необходимости в написа­ нии отдельного кода для совместного использования. В нашем случаг мы создаем совместный файл кода ShareCode. Другим отличием в приведенном методе являются необходимые длл описания приложения флажки. В нашем случае используются флажки ClientServer и NoDpnServer. Первый флажок достаточно очевиден, вто­ рой же требует пояснений. Как уже упоминалось выше, к отдельному порту одновременно мо­ жет обращаться только одно приложение. Однако, существует поддер­ живаемый DirectPlay сценарий выполнения на машине нескольких сер­ верных программ, оперирующих с одним и тем же портом. Для работы в таком режиме в DirectPlay имеется внешнее приложе­ ние «dpnsrv.exe», которое запускается при первом создании соединения. Данное приложение привязывается к определенному или заданному по умолчанию порту и пересылает любые клиентские данные на «реаль­ ный» хост-компьютер. Иногда использование данного режима весьма полезно, но для нашего приложения в этом нет необходимости, и мы дол­ жны использовать флажок NoDpnServer для игнорирования вызова при­ ложения «dpnsrv.exe». Прежде чем запускать и использовать сервер, мы должны убедиться в корректной работе процедур инициализации и завершения соединения в конце сеанса. Перепишите основной метод следующим образом:

static void Main() { using (Forml frm = new Form1()) { frm. Show ();

frm.InitializeServer();

Application.Run(frm);

} } Также необходимо изменить функцию Dispose (освобождение объек­ та) для нашей формы, добавив соответствующий код:

Глава 19. Создание сессии Client/Server if (connection != null) connection.Dispose ();

Чтобы почувствовать влияние использования флажка NoDpnServer, нужно откомпилировать и запустить приложение. Мы увидим сообще­ ние о том, что наше соединение установлено и является главным. Те­ перь, не прерывая сеанс работы первого примера, попробуем запустить приложение еще раз. Мы должны получить сообщение об ошибке и не­ возможности соединения. Причина кроется в том, что второй экземпляр запускался при обращении к уже использующемуся порту. Теперь удали­ те флажок, и попробуйте проделать то же самое. Результатом будет одно­ временная работа двух хостов на нашем сервере. Теперь было бы желательно вернуть на место удаленный флажок NoDpnServer и продолжить написание программы.

Создание соединения клиент-сервер Создав выделенный сервер, попробуем теперь создать соединение клиент-сервер. В принципе, на данном этапе можно было бы добавить процедуру создания клиента в программу для сервера, но поскольку мы хотим иметь выделенный сервер, запишем отдельный код для клиентс­ кой части. Для этого нам понадобится сконструировать еще одну windowsформу. Пользовательский интерфейс и свойства диалогового окна клиента будут такими же, как и для сервера. Следует отметить, что для клиентс­ кой части необходимо выделить больше места в форме под кнопки ос­ новных операций. Также нашему соединению понадобится имя. Для этого создадим в форме окно (аналогично плашке textbox), в котором сможем изменять имя клиента. Далее мы должны удостовериться, что имеем доступ к общим дан­ ным сервера и клиента. При необходимости нужно добавить ссылку на общий файл кода ShareCode, который был создан нами для сервера. При добавлении данной ссылки убедитесь в том, что нажали маленькую стрел­ ку на кнопке «Open», и выбрали опцию Link file (тогда файл кода будет формироваться в отдельной папке, иначе в локальной папке будет созда­ на копия файла, и данные более не будут доступны для совместного ис­ пользования нашими приложениями). Выполнив подготовительные действия, мы можем приступить к на­ писанию кода для клиентской части, объявив в начале необходимые ло­ кальные переменные:

private Client connection = null;

private bool connected = false;

Часть VI. Добавление сетевых возможностей Так же как и для Р2Р-сеанса, для проверки наличия соединения меж­ ду клиентом и сервером нам понадобится соответствующая переменная. Как обычно, запишем метод инициализации, включающий в себя воз­ можность поиска имеющихся в сети хостов. Добавьте метод, приведен­ ный в листинге 19.2 к нашему клиентскому приложению.

Листонг 19.2. Инициализация соединения Client. public void InitializeClient() { // Create our client object connection = new Client));

// Hook the events we want to listen for connection.ConnectComplete += new ConnectCompleteEventHandler(OnConnectComplete);

connection.FindHostResponse += new FindHostResponseEventHandler(OnFindHost);

// Check to see if we can create a TCP/IP connection if (!IsServiceProviderValid(Address.ServiceProviderTcpIp)) { // Nope, can't, quit this application MessageBox.Show("Could not create a TCP/IP service provider.", "Exiting" MessageBoxButtons.OK, MessageBoxIcon.Information);

this. Close();

} // Create a new address for our local machine Address deviceAddress = new Address();

deviceAddress.ServiceProvider = Address.ServiceProviderTcpIp;

// Create a new address for our host machine Address hostaddress = new Address();

hostaddress.ServiceProvider = Address.ServiceProviderTcpIp;

hostaddress.AddComponent(Address.KeyPort, SharedCode.DataPort);

// Set our name Playerlnformation info = new Playerlnformationf);

info.Name = textBoxl.Text;

connection.SetClientlnformation(info, SyncFlags.Clientlnformation);

// Set up an application description ApplicationDescription desc = new ApplicationDescription();

desc.GuidApplication = SharedCode.ApplicationGuid;

try { // Search for a server connection.FindHosts(desc, hostaddress, deviceAddress, null, 0, 0, 0, FindHostsFlags.None);

AddText("Looking for sessions.");

} Глава 19. Создание сессии Client/Server catch { AddText("Enumeration of sessions has failed.");

} } Здесь имеется много общего с тем, что мы уже делали в предыдущей главе. После создания клиента приложение производит поиск хостов, затем пытается установить соединение с ними, затем проводится про­ верка наличия службы поддержки доступа. Эти вопросы мы уже рассмат­ ривали при написании программы для Р2Р-сети. Далее создаются адреса TCP/IP для локальной машины и отдельный адрес для сервера. Как мы уже знаем, любой главный компьютер, к кото­ рому мы будем подключаться, будет задействовать порт, определенный в SharedCode.DataPort. Добавим этот компонент, а также имя хоста (если оно нам известно) к адресу сервера. Каждый "участник" сеанса в DirectPlay может иметь некоторую спе­ цифическую информацию, которая может содержать имя "игрока" или описание приложения. Например, если мы хотим изменить имя клиентс­ кого приложения, которым на данный момент является слово "textboxl", мы можем сделать это с помощью следующего выражения:

textBoxl.Text = System.Environment.UserName;

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

ПОИСК В ЛОКАЛЬНОЙ ПОДСЕТИ • Обратите внимание, что в этом примере мы не использовали флажок FindHostsFlags.OkToQueryForAddressing. Мы полагали, что сер­ вер находится в той же самой подсети (значение по умолчанию для DirectPlay). В противном случае мы можем использовать этот фла­ жок или непосредственно добавить имя сервера в качестве компо­ нента его адреса.

Затем мы вызываем процедуру описания приложения. Обратите вни­ мание, что единственным параметром, который мы устанавливаем в этом случае, является идентификатор GUID. Этот идентификатор должен быть одним и тем же и для сервера, и для клиента. И, наконец, выпол­ няется вызов метода FindHosts, сопровождающийся надписью «поиск соединения».

Часть VI. Добавление сетевых возможностей Теперь нам понадобятся две процедуры установки и завершения со­ единения, которые мы объявили вначале. Соответствующий код приве­ ден в листинге 19.3. Листинг 19.3. Обработчик для установки и завершения соединения. private void OnFindHost(object sender, FindHostResponseEventArgs e) { lock(this) { // Do nothing if we're connected already if (connected) return;

connected = true;

string foundSession = string.Format ("Found session ({0}), trying to connect.", e.Message. ApplicationDescription. SessionName) ;

this.Beginlnvoke(new AddTextCallback(AddText), new object[] { foundSession });

// Connect to the first one ((Client)sender).Connect(e.Message.ApplicationDescription, e.Message.AddressSender, e.Message.AddressDevice, null, ConnectFlags.OkToQueryForAddressing);

} } private void OnConnectComplete(object sender, ConnectCompleteEventArgs e) { // Check to see if we connected properly if (e.Message.ResultCode == ResultCode.Success) { this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( "Connect Success." lbconnected = true;

this.Beginlnvoke(new EnableCallback(EnableSendDataButton), new object[] { true ) );

} else { this.Beginlnvoke(new AddTextCallback(AddText), new object[] { string.Format("Connect Failure: {0}", e.Message.ResultCode) });

connected = false;

this.Beginlnvoke(new EnableCallback(EnableSendDataButton), new object[] ( false } );

} } Глава 19. Создание сессии Client/Server Методы достаточно знакомы нам по сценариям Р2Р-соединения. Вна­ чале мы пытаемся найти в сети доступные нам серверы, после чего пы­ таемся к ним подключиться. При завершении соединения соответствую­ щий обработчик отслеживает это событие. Вызов операции dispose в дан­ ном случае не используется, поскольку это бьшо сделано при написании серверной части (вернее, общего для обеих частей фрагмента кода). Те­ перь необходимо переписать наш основной метод следующим образом:

static void Main() { using (Forml frm = new Forml()) { frm.Show();

frm.InitializeClient();

Application.Run(frm);

} } Таким образом, наш «клиент» готов к работе. Теперь можно запус­ тить оба приложения. Мы должны увидеть, что сервер сообщит нам о начале работы, а клиент после обнаружения сервера присоединится к нему. Попробуем теперь слегка разнообразить наши приложения.

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

connection.PlayerCreated += new PlayerCreatedEventHandler(OnPlayerCreated);

connection.PlayerDestroyed += new PlayerDestroyedEventHandler(OnPlayerDestroyed);

Событие регистрируется всякий раз, когда игрок присоединяется или покидает сеть. В листинге 19.4 приведен код указанных процедур, до­ бавьте его в приложение для сервера.

Листинг 19.4. Обработчик клиента. private void OnPlayerCreatedfobject sender, PlayerCreatedEventArgs e) { 358 try { Часть VI. Добавление сетевых возможностей string playerName = ((Server)sender).GetClientlnforaation (e.Message.PlayerlD).Name;

string newtext = string.Format ("Accepted new connection from (0), UserlD: 0x{l}", playerName, e.Message.PlayerlD.ToString("x")) ;

this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( newtext ));

} catch { /* Ignore this, probably the server */ } } private void OnPlayerDestroyed(object sender, PlayerDestroyedEventArgs e) { string newtext = string.Format ("DirectPlayer UserlD: 0x{0} has left the session.", e.Message.PlayerlD.ToString("x"));

this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( newtext ));

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

в серверной части мы ищем это имя с помощью процедуры GetClientlnfomiation. При первом запуске сервера по умолчанию создается «скрытый» игрок, и, пока не появится соответствую­ щий ему клиент, процедура SetClientlnformation игнорируется. КОНТРОЛЬ ДОСТУПА В СЕТЬ Вполне возможно, что вам может понадобиться запретить доступ в сеть для некоторых пользователей (например, даже для тех, кто уже знает имя сервера, идентификатор GUID и порт). Помимо обработчика операции входа в сеть имеется функция IndicateConnect, аргументом которой (IndicateConnectEventArgs) яв­ ляется параметр RejectMessage (по умолчанию со значением «false»). Опираясь на значение параметра этого обработчика, мы можем либо позволить, либо не позволить выполнение этого соеди­ нения. В первом случае нам не нужно ничего предпринимать допол­ нительно, сеанс выполнится автоматически. Во втором случае мы просто устанавливаем параметр RejectMessage в значение «true».

Глава 19. Создание сессии Client/Server Что если мы вообще хотим сделать вызов сервера недоступным? Как нам известно, при нахождении сервера или хоста пользователь принимает информацию FindHostsResponse, но перед этим сервер в свою очередь получит обращение FindHostsQuery. Таким образом, мы можем использовать это событие, отследив его и применив уже описанную стратегию. Используя комбинации соответствующих процедур, мы можем осуществить полный контроль всей сети. Следует отметить, что хост-компьютеры для пиринговых сетей име­ ют аналогичные возможности.

Передача пакетов данных по сети В главе 19, описывая Р2Р-сети, мы уже создали некоторую процедуру для передачи простого пакета данных, если быть точнее, одного байта данных. При работе сетей «клиент-сервер» трафик обмена данными мо­ жет быть весьма значительным. Для моделирования реального приложения мы можем добавить к кли­ ентскому приложению несколько опций, которые позволят облегчить от­ правку пакетов данных на сервер. Для проверки мы можем выполнить несколько стандартных действий. Определить число игроков в сети. • Послать сообщения другим игрокам, находящимся в сети. • Послать сообщение на сервер. • Стать недоступным для других участников сети. • Изменить имя клиента. Очевидно, что и клиентское, и серверное приложения должны «знать» об этих действиях, для этого необходимо добавить перечень указанных действий к файлу кода ShareCode:

public enum NetworkMessages { CheckPlayers, Wave, SendData, RunAway, ChangeName, } Теперь у нас имеется отдельное значение для каждого действия. Для того чтобы клиент мог выполнить указанные действия, создайте пять соответствующих кнопок в верхней части нашей формы. Для упрощения программы используем одну процедуру для всех пяти кнопок. Этот ме­ тод приведен в листинге 19.5.

Часть VI. Добавление сетевых возможностей Листинг 19.5. Обработка кнопок для отправки пакетов данных. private void button_Click(object sender, System.EventArgs e) { // We want to send some data to the server NetworkPacket packet = new NetworkPacket();

// Write the correct message depending on the button pressed switch(((Button)sender).Name) { case "buttonl": packet.Write(NetworkMessages.CheckPlayers);

break;

case "button2": packet.Write(NetworkMessages.Wave);

break;

case "button3": packet.Write(NetworkMessages.SendData);

break;

case "button4": packet.Write(NetworkMessages.RunAway);

break;

case "button5": if (textBoxl.Text.Length > 0) { packet.Write(NetworkMessages.ChangeName);

packet.Write(textBoxl.Text);

Playerlnformation info = new Playerlnformation();

info.Name = textBoxl.Text;

connection.SetClientInformation(info, SyncFlags.Clientlnformation);

} else { // Don't try to do anything if there is no name return;

} break;

} connection.Send(packet, 0, SendFlags.Guaranteed);

} В зависимости от предпринятых действий, в сетевой пакет записывает­ ся соответствующее сообщение. Если мы нажмем кнопку «ChangeName» и введем некоторый текст в диалоговое окно, мы не только запишем это сообщение в пакет данных, но и изменим соответствующим образом ин­ формацию о клиентском приложении.

Глава 19. Создание сессии Client/Server Наконец, данные отправляются на сервер. Обратите внимание, что здесь нет никакого пользовательского идентификатора для клиента, по­ скольку в соединении клиент-сервер клиент может посылать данные толь­ ко на сервер. Теперь, подготовив клиентскую часть к отправке данных, необходи­ мо подготовить сервер к приему и обработке этих данных. Естественно, в первую очередь мы должны зафиксировать событие поступления дан­ ных на сервер. Добавьте следующую строку к методу инициализации сервера:

connection.Receive += new ReceiveEventHandler(OnDataReceive);

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

Листинг 19.6. Обработчик для принимаемых на сервере данных. private void OnDataReceive(object sender, ReceiveEventArgs e) [ NetworkMessages msg = (NetworkMessages)e.Message.ReceiveData.Read (typeof(NetworkMessages));

NetworkPacket returnedPacket = new NetworkPacketO;

string newtext = string.Empty;

switch (msg) ( case NetworkMessages.ChangeName: string newname = e.Message.ReceiveData.ReadStringO;

newtext = string.Format ("DPlay Userld 0x{0| changed name to {1}", e.Message.SenderlD.ToStringPx"), newname);

// The user wants inform everyone they ran away returnedPacket.Write(NetworkMessages.ChangeName);

returnedPacket.Write(e.Message.SenderlD);

returnedPacket.Write(newname);

// Now send it everyone connection.SendTo((int)PlayerID.AHPlayers, returnedPacket, 0, SendFlags.Guaranteed ! SendFlags.NoLoopback);

break;

case NetworkMessages.CheckPlayers: newtext = string.Format ("Received CheckPlayers from DPlay Userld: Ox{0}", e.Message.SenderlD.ToString("x"));

// The user wants to know how many players are in the session Часть VI. Добавление сетевых возможностей returnedPacket.Write(NetworkMessages.CheckPlayers);

// subtract one user for the server user returnedPacket.Write(connection.Players.Count - 1);

// Now send it only to that player connection.Sendlofe.Message.SenderlD, returnedPacket, 0, SendFlags.Guaranteed);

break;

case NetworkMessages.RunAway: newtext = string.Format ("Received RunAway from DPlay Userld: 0x{0}", e.Message.SenderlD.ToString("x"));

// The user wants inform everyone they ran away returnedPacket.Write(NetworkMessages.RunAway);

returnedPacket.Write(e.Message.SenderID) ;

// Now send it everyone connection.SendTo((int)PlayerID.AllPlayers, returnedPacket, 0, SendFlags.Guaranteed | SendFlags.NoLoopback);

break;

case NetworkMessages.SendData: newtext = string.Format ("Received SendData from DPlay Userld: 0x{0}", e.Message.SenderlD.ToString("x"));

// No need to reply, 'fake' server data break;

case NetworkMessages.Wave: newtext = string.Format ("Received Wave from DPlay Userld: 0x{0}", e.Message.SenderlD.ToString("x"));

// The user wants inform everyone they waved returnedPacket.Write(NetworkMessages.Wave);

returnedPacket.Write(e.Message.SenderlD);

// Now send it everyone connection.SendTo( (int)PlayerlD.AHPlayers, returnedPacket, 0, SendFlags.Guaranteed \ SendFlags.NoLoopback);

break;

} // We received some data, update our UI this.Beginlnvoke(new AddTextCallback(AddText), new object[] ( newtext });

} Этот метод не так сложен, как может показаться. Одним из его пара­ метров является передаваемый сетевой пакет. Аналогично вызову Write (когда мы размещали данные в пакет) для извлечения этих данных вызы­ вается метод Read.

Глава 19. Создание сессии Client/Server ЧТЕНИЕ ДАННЫХ В УСТАНОВЛЕННОМ ПОРЯДКЕ Обратите внимание, что данные должны читаться в том же самом порядке, в котором они были записаны. В противном случае при работе приложения могут возникнуть ошибки. Метод Read должен иметь в качестве входного параметра тип полу­ ченных нами данных, который мы должны предварительно определить. Затем мы устанавливаем необходимость отправки ответа либо опреде­ ленному клиенту, либо всем участникам сети. Для отправки данных мы используем самое простое действие — SendData. Данная процедура не требует обратной связи (за исключением сообщения собственно на самом сервере). Действия Wave и RunAway идентичны друг другу. При получении любого из этих сообщений вначале, как обычно, обновляется пользова­ тельский интерфейс на сервере, далее формируется новый пакет данных, куда записывается полученная информация и идентификатор пославше­ го его пользователя. Этот пакет рассылается всем участникам сеанса свя­ зи. Обратите внимание, что при отправке данных с сервера мы использу­ ем флажок NoLoopBack, который позволяет избежать получения ответ­ ных сообщений. Действие CheckPlayers отличается от остальных. Клиентское прило­ жение запрашивает на сервере необходимую только ему информацию. Сервер снова записывает идентификатор сообщения в новый сетевой пакет, затем указывает число игроков, находящихся в сети (исключая вир­ туальных игроков на сервере), и, наконец, отправляет новый пакет не­ посредственно отправителю запроса, так что никакие другие игроки в сети не могут увидеть это сообщение (если только они не запрашивают ту же самую информацию). В завершение на сервере обновляется пользо­ вательский интерфейс. Сценарий последнего действия похож на сценарии Wave и RunAway. С помощью метода ReadString мы извлекаем информацию о новом име­ ни клиента непосредственно из сетевого пакета. Единственное главное отличие от описанных выше сценариев Wave и Run away заключается в том, что после включения в новый пакет данных идентификаторов сооб­ щения и отправителя, мы также записываем туда новое имя клиента, ини­ циировавшего указанное действие.

Формирование отклика клиента Теперь мы можем написать процедуру, позволяющую отследить при­ нятие данных клиентом. Добавьте в приложение Client фрагмент кода, похожий на тот, который мы использовали для тех же целей на сервере, см. листинг 19.7.

Часть VI. Добавление сетевых возможностей Листинг 19.7. Обработчик получения данных клиентом. private void OnDataReceive(object sender, ReceiveEventArgs e) { NetworkMessages msg = (NetworkMessages)e.Message.ReceiveData.Read (typeof(NetworkMessages));

string newtext = string.Empty;

int playerlD = 0;

switch (msg) { case NetworkMessages.ChangeName: playerlD = (int)e.Message.ReceiveData.Readftypeof(int));

string newname = e.Message.ReceiveData.ReadString();

newtext = string.Format ("DPlay Userld 0x(0( changed name to {1}", playerlD.ToString(Y), newname);

break;

case NetworkMessages.CheckPlayers: int count = (int)e.Message.ReceiveData.Readftypeof(int));

newtext = string.Format ("Server reports {0} users on the server currently.", count) ;

break;

case NetworkMessages.RunAway: playerlD = (int)e.Message.ReceiveData.Readftypeof (int));

newtext = string.Format ("Server reports DPlay Userld 0x{0} has ran away.", playerlD. ToString(Y));

break;

case NetworkMessages.Wave: playerlD = (int)e.Message.ReceiveData.Readftypeof(int));

newtext = string.Format ("Server reports DPlay Userld 0x{0} has waved.", playerlD. ToString(Y));

break;

) / / W e received some data, update our UI this.Beginlnvoke(new AddTextCallback(AddText), new object[] { newtext });

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

Глава 19. Создание сессии Client/Server Обработка отключения сервера Последнее, что мы должны сделать для созданного соединения, — пра­ вильно отследить момент потери или отсоединения сервера. Нечто по­ добное мы уже проделывали при работе с Р2Р-сетями. Вначале мы до­ бавляем определение этого события (SessionTerminated): connection.SessionTerminated += new SessionTerminatedEventHandler(OnSessionTerminate);

И записываем код для этого обработчика: private void OnSessionTerminate(object sender, SessionTerminatedEventArgs e) { this.Beginlnvoke(new DisconnectCallback(OnDisconnect), null);

} private void OnDisconnect() { EnableSendDataButtons(false);

AddText("Session terminated.") ;

connected = false;

// Dispose of our connection, and set it to null connection.Dispose();

connection = null;

} Данная процедура проверяет факт отсоединения и освобождает объект. Также выводится сообщение о том, что соединение разорвано.

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

Часть VI. Добавление сетевых возможностей Глава 20. Особенности более совершенного использования сетей Эта глава охватывает дополнительные возможности DirectPlay, кото­ рые позволят нам расширить диапазон использования сетей, и включает следующие разделы. • Модели событий. Пропускная способность, трафик. • Очередность отправки данных. Приложения лобби «lobby-launching». Использование голосовой связи.

Модели событий и обработчики К настоящему моменту мы изучили работу Р2Р-сетей и основы архи­ тектуры клиент-сервер. Теперь мы можем попытаться расширить диапа­ зон наших знаний относительно использования сетей. До сих пор мы рассматривали лишь некоторые из возможных в тече­ ние сеанса связи событий. Ниже приведен список практически всех со­ бытий, доступных для обработки с помощью DirectPlay, с указанием клас­ сов, для которых применимы данные процедуры. ApplicationDescriptionChanged — применяется для классов Peer, Server и Client. Отслеживается событие, когда изменяется описа­ ние объекта (например, меняется имя). AsyncOperationComplete — применяется для классов Peer, Server и Client. Фиксирует завершение асинхронной операции. Некото­ рые функции DirectPlay (например, Connect или Receive) возвра­ щают параметр, который сообщает приложению о завершении выполнения. С помощью этого параметра соответствующие ме­ тоды можно отменить, используя процедуру CancelAsyncOperation. Необходимо помнить, что некоторые операции могут заканчивать­ ся автоматически, как, например, метод FindHosts при установке соединения. • ConnectComplete — применяется для классов Peer и Client. Собы­ тие возникает после неудачной попытки соединения. Значение па­ раметра ResultCode указывает на причину возникшей ситуации. Например, если хост отклонил ваш запрос о подключении, резуль­ татом будет значение HostRejectedConnection. FindHostQuer — применяется для классов Peer и Server. Событие фиксируется, когда новое клиентское приложение вызывает ме­ тод FindHosts и начинает выполнять поиск хоста. Если вы не из­ мените значение параметра RejectMessage (по умолчанию «false») перед тем, как ответить на запрос, метод FindHosts успешно обна Глава 20. Особенности более совершенного использования сетей ружит ваш хост. Установка аргумента RejectMessage в значение «true» сделает ваш сеанс недоступным для клиента. • FindHostResponse — применяется для классов Peer и Client. От­ слеживается событие обнаружения хоста. Возвращаемый параметр будет содержать информацию (адрес и описание хоста), доста­ точную для того, чтобы соединиться с найденным сервером. Если не происходит немедленного соединения с сервером, поиск будет продолжаться, и вполне возможно, что один и тот же хост будет найден несколько раз. GroupCreated — применяется для классов Peer и Server. Возника­ ет в результате успешного выполнения процедуры создания групп игроков CreateGroup. • GroupDestroyed — применяется для классов Peer и Server. Отсле­ живается событие расформирования группы (например, в резуль­ тате использования метода DestroyGroup или при разрыве соеди­ нения). Если при создании группы использовался флажок AutoDestruct, группа будет автоматически расформирована, если один из участников завершил сеанс. Причину расформирования группы можно найти в параметре Reason. • Grouplnformation — применяется для классов Peer и Server. Когда изменяется информация о группе (например, при помощи метода SetGroupInformation), это событие регистрируется всеми ее участ­ никами. В качестве параметра возвращается только идентификатор группы, поэтому не забудьте запросить остальную информацию. • HostMigrated — обработчик применим только к классу Peer. Если в течении Р2Р-сеанса главный компьютер вышел из соединения, статус хоста передается следующему компьютеру. Событие про­ исходит, только если при создании хоста был включен соответ­ ствующий флажок. • IndicateConnect — применяется для классов Peer и Server. Собы­ тие, аналогичное событию FindHostQuery (соединение также мо­ жет быть запрещено). • IndicateConnectAborted — применяется для классов Peer и Server. Как правило, за событием IndicateConnect должно следовать со­ бытие PlayerCreated. Однако, если по каким-либо причинам со­ единение было прервано до этого момента (например, случайный разрыв соединения), произойдет указанное событие. • Peerlnformation — обработчик применим к классу Peer. Отслежи­ вает изменение данных объекта peer. При необходимости следует восстановить остальную информацию. • PlayerAddedToGroup — применяется для классов Peer и Server. Происходит, когда новый игрок (или группа игроков) добавляется к существующей группе. Использует идентификаторы группы и игрока.

368 • Часть VI. Добавление сетевых возможностей PlayerCreated — применяется для классов Peer и Server. Данное событие следует за событием IndicateConnect. В момент выпол­ нения обработчика в качестве аргумента можно устанавливать контекстную переменную для игрока (любая специфическая ин­ формация об игроке). Каждый раз, получая данные от игрока, можно получать и контекстную переменную этого игрока. • PlayerDestroyed — применяется для классов Peer и Server. Собы­ тие фиксируется, когда игрок покидает сеанс связи. Когда же за­ вершается сам сеанс связи, мы получаем подобное сообщение от каждого клиента. PlayerRemovedFromGroup — применяется для классов Peer и Server. Происходит при удалении игрока из группы (например, с помощью вызова RemovePlayerFromGroup) или выходе игрока из сеанса. Receive — применяется для классов Peer, Server и Client. Инфор­ мирует о получении новых данных (сетевого пакета, идентифи­ катора игрока, контекстной переменной игрока, отправляющего информацию и т. д.). Обратите внимание на использование флаж­ ка NoLoopback, который мы уже описывали раньше. • SendComplete — применяется для классов Peer, Server и Client. Возникает в результате завершения асинхронной операции пере­ дачи данных. Если используется флажок CompleteOnProcess, со­ бытие будет зарегистрировано только после получения сообще­ ния объектом, которому вы его направляете. Если используется флажок NoComplete, событие не будет обработано. SessionTerminated — применяется для классов Peer и Client. Со­ бытие отслеживается каждый раз при завершении сеанса. Полу­ чить информацию о причине завершения соединения можно с помощью параметра ResultCode. Может содержать определяемую пользователем информацию (например, для организации другого подключения) Clientlnformation — применяется для классов Server и Client. От­ слеживается момент изменения информации о клиенте. При не­ обходимости информацию следует восстановить после регистра­ ции события. • Serverlnformation — применяется для классов Server и Client. Отсле­ живает изменение информации о сервере. При необходимости тре­ буется восстановление информации после регистрации события.

Определение пропускной способности и статистики сети На сегодняшний день вопросы пропускной способности соединений являются весьма актуальными. Подключение к Internet с помощью моде Глава 20. Особенности более совершенного использования сетей ма имеет достаточно низкую пропускную способность. Сеть на базе Ethernet более предпочтительна и является наиболее распространенной на данный момент, но и здесь также имеются ограничения. Задача разра­ ботчика сетевых приложений состоит в том, чтобы самым рациональ­ ным образом распределять трафик, добиваясь максимальной пропуск­ ной способности соединения. При написании серверной части для сетевой игры необходимо пре­ дусмотреть максимальную нагрузку на канал сервера, поскольку подра­ зумевается поддержка и обмен данными с тысячами пользователей одно­ временно. В этом случае работа над пропускной способностью сервера отличается от той же работы для клиента. DirectPlay API достаточно универсален и позволяет оптимальным об­ разом распределять пересылаемые клиентам данные. Для получения ин­ формации о состоянии имеющихся подключений необходимо, указав иден­ тификатор запрашиваемого игрока, вызвать метод GetConnectionlnformation. Клиент возвратит информацию о соединении с сервером. Ниже приведен пример такой информации:

Connection information: PacketsDropped: 0 BytesDropped: 0 PacketsRetried: 0 BytesRetried: 0 PacketsSentNonGuaranteed: 1 BytesSentNonGuaranteed: 0 PacketsSentGuaranteed: 99 BytesSentGuaranteed: 1344 PeakThroughputBps: 116 ThroughputBps: 80 RoundTripLatencyMs: 4 MessagesReceived: 79 • PacketsReceivedNonGuaranteed: 0 BytesReceivedNonGuaranteed: 0 PacketsReceivedGuaranteed: 79 BytesReceivedGuaranteed: 1836 MessagesTimedOutLowPriority: 0 MessagesTransmittedLowPriority: 0 MessagesTimedOutNormalPriority: 0 MessagesTransmittedNormalPriority: 99 MessagesTimedOutHighPriority: 0 MessagesTransmittedHighPriority: В этом блоке содержится практически вся информация о соединении: число пакетов, принятых, отправленных, имеющих различные статусы и флажки и т. д.

13 Зак. Часть VI. Добавление сетевых возможностей Здесь же можно найти информацию о максимальной (PeakThroughputBps) и средней (ThroughputBps) пропускной способности канала, измеряемой в байтах за секунду. Данная информация позволяет проверить, насколько оп­ тимально загружается канал. Под средним временем ожидания (RoundTripLatencyMs) можно по­ нимать время, затраченное на отправку пакета и на пересылку его обрат­ но. Данный параметр коррелирует с пропускной способностью, но при этом позволяет отследить временные затраты для данного конкретного канала. В завершение мы получаем информацию о количестве сообщений, полученных в различные моменты времени и имеющих различные при­ оритетные уровни (нормальный, низкий и высокий). Необходимо отметить, что DirectPlay не будет посылать сообщения удаленному компьютеру быстрее, чем тот может их обработать. Если удаленный компьютер не отвечает долгое время, отправитель создаст очередность отправляемых данных. Перед отправкой данные могут быть сгруппированы в пакеты, которые, в свою очередь, могут быть объеди­ нены. Вы можете определить количество данных в очереди, вызывая метод GetSendQueuelnformation (выполняется аналогично методу GetConnectionlnformation для сервера или Peer-объекта). При этом зап­ рос возвращает два целых числа (для каждого из объектов), первое — число сообщений, находящихся в очереди, и второе — суммарное чис­ ло байтов для этих сообщений. Также возможно управлять приоритетом отсылаемых данных, исполь­ зуя соответствующие флажки GetSendQueuelnformationFlags. Это позво­ ляет отследить момент переполнения очереди и ограничить количество пересылаемых данных. Наибольшее внимание при настройке соединений следует уделить устранению «ненужных» данных, которые передаются по каналу (это касается пересылки без необходимости строк, булевых переменных и пр.). Вместо того чтобы отправить четыре логические переменные, попробуйте отправить один байт с побитовой маскировкой для различных булевых констант. Например:

private private private private bool bool bool bool IsRunning = true;

IsMale = false;

IsWarrior = true;

IsGhost = false;

Если отправлять данные в таком виде, передаваемый пакет будет иметь размер, равный 16-ти байтам, что является весьма расточительным в плане трафика. Наша задача снизить это значение, по крайней мере, до одного байта:

Глава 20. Особенности более совершенного использования сетей private private private private private const byte IsRunning = Oxl;

const byte IsMale = 0x2;

const byte IsWarrior = 0x4;

const byte IsGhost = 0x8;

byte SendData = IsRunning | IsWarrior;

Теперь однобайтовая переменная SendData содержит информацию, соответствующую четырем булевым переменным. Таким способом мы можем «замаскировать» до 8-ми логических переменных (экономя при этом 31 байт). Если необходимо переслать более чем 8 переменных, можно использовать короткий формат, который может поддерживать до 16-ти булевых переменных (экономя 62 байта) или длинный формат, который может поддерживать до 64-х булевых переменных (экономя 252 байта). Для уменьшения трафика при возможности необходимо использовать наименьший формат.

Запуск приложений, использующих концепцию «Lobby» Для того, кто когда-либо запускал игры типа MSN Games (сайт http:// zone.msn.com), концепция «лобби» достаточно понятна. По существу, лобби — это подход, когда группа игроков собирается перед запуском игры, и игра запускается одновременно для всех игроков, при этом все игроки автоматически соединяются друг с другом. Вспоминая сборки для DirectPlay, необходимо заметить, что это — пер­ вая используемая нами сборка, имеющая подпространство имен. Внутри этой сборки имеются два дополнительных пространства имен: Lobby и Voice. Свойства второго списка мы рассмотрим позже, а пока сконцент­ рируемся на пространстве имен Lobby. Пространство Lobby включает два основных класса, которые будут управлять взаимодействиями в лобби-приложениях: Application и Client. Класс Client используется для запуска и поддержки лобби-приложений на удаленных машинах, а класс Application непосредственно управляет лобби-приложением. Каждый из упомянутых классов имеет модели со­ бытий, подобно тем моделям, которые используются в классах Peer, Server и Client. Вначале нам необходимо определить программы, которые могут за­ пускаться в режиме Lobby, а уже потом попробовать запустить их. Как обычно, создаем новое окно приложения, устанавливаем параметр Dock в значение «Fill», проверяем добавление ссылок на DirectPlay и директи­ ву using для пространства имен Lobby:

using Microsoft.DirectX.DirectPlay.Lobby;

Часть VI. Добавление сетевых возможностей Отдельно объявляем переменную класса Client, поскольку именно этот класс отвечает за объекты и запуск лобби-приложения:

private Client connection = null;

Так же мы должны освободить объект после выхода из соединения, добавив две строки к соответствующей перегрузке Dispose: if (connection != null) connection.Dispose ();

Следует отметить, что имя класса Client встречается и в пространстве имен DirectPlay, и в пространстве имен Lobby. Таким образом, мы долж­ ны однозначно определить оба этих пространства и включить их в ди- • рективу using. Это же касается и другого класса (Application), который уже включен в пространство имен System.Windows.Forms. Теперь давайте заполнять окно списка «list box» для наших лоббиприложений. Добавьте следующий код к вашему конструктору форм:

// Fill the list box connection = new Client();

foreach(ApplicationInformation ai in connection.GetLocalPrograms()) { listBoxl.Items.Add(ai.ApplicationName);

} Как видите, это относительно простая операция. После создания лоб­ би-объекта Client происходит перебор всех имеющихся локальных про­ грамм, и название каждой из них записывается в наше окно. Запуск этих приложений не сложен;

добавим процедуру обработки двойного нажа­ тия кнопки мыши в окне списка. Для этого используем код, приведенный в листинге 20.1.

Листинг 20.1. Запуск приложения. private void HstBoxl_DoubleClick(object sender, System.EventArgs e) } if (listBoxl.Selectedltem == null) return;

foreachfApplicationlnformation ai in connection.GetLocalPrograms()) { if (ai.ApplicationName == (string)listBoxl.Selectedltem) { Connectlnformation ci = new Connectlnformation();

Глава 20. Особенности более совершенного использования сетей ci.GuidApplication = ai.GuidApplication;

ci.Flags = ConnectFlags.LaunchNew;

connection.ConnectApplication(ci, System.Threading.Timeout.Infinite, null);

break;

} } } Этот фрагмент кода — не самый эффективный, поскольку нам при­ дется проходить список дважды, но весьма показательный. Если имеется выбранный нами пункт, мы находим его, после чего инициализируется структура Connectlnformation с идентификатором GUID и управляющи­ ми флажками. ПРИСОЕДИНЕНИЕ К С СУЩЕСТВУЮЩЕЙ СЕССИИ ИЛИ СЕАНСУ Мы также можем присоединиться к уже выполняемому приложению, имеющему статус лобби. Для этого необходимо установить флажок ConnectFlags.LaunchNotFound в структуре ConnectionSettings (если действующее приложение не будет найдено, запустится новый эк­ земпляр). Следует отметить, что при компиляции появится сообщение о нео­ пределенности класса Application, поскольку класс Application встреча­ ется и в System.Windows.Forms, и в Microsoft.DirectX.DirectPlay.Lobby. По этой причине перепишем основную процедуру следующим образом: static void Main() { using (Forml frm = new Forml()) { frm.Show();

System.Windows.Forms.Application.Run(frm);

} } После успешного запуска приложения метод ConnectApplication воз­ вращает обработчик соединения, с помощью которого мы можем отпра­ вить данные в приложение, используя метод Send, или вызывать метод ReleaseApplication, который завершит работу приложения (но это приве­ дет не к закрытию приложения, а к отсоединению лобби-клиента от дан­ ного приложения).

Часть VI. Добавление сетевых возможностей Создание лобби-приложения Класс Application в пространстве имен Lobby определяет структуру информации, доступную после запуска лобби-сеанса. Регистрация лоб­ би-приложения довольно проста. Необходимо создать и заполнить струк­ туру ProgramDescription, обязательно указав идентификатор GUID для этого приложения, имя, путь и параметры файла. Затем мы можем выз­ вать либо метод RegisterProgram, либо метод UnregisterProgram в зависи­ мости от выполняемой операции. До сих пор мы подробно не рассматривали конструктор объекта Application для лобби-соединения. Обсудим в качестве примера вариант, содержащий наибольшее число параметров: public Application ( System.Int32 connectionHandle, Microsoft.DirectX.DirectPlay.Lobby.InitializeFlags flags, Microsoft.DirectX.DirectPlay.Lobby.ConnectEventHandler connectEventHandler ) Параметр обработчика подключения является выходным параметром. Если ваше приложение было запущено в качестве лобби-клиента, этот обработчик будет возвращен процедурой ConnectApplicaton, в против­ ном случае он будет не определен. Использование флажков InitializeFlags позволяет отключить проверку этого параметра. Последний параметр ConnectEventHandler —позволяет отслеживать событие подключения еще до создания объекта. После запуска лобби-клиента необходимо вызвать метод RegisterLobby для подключения к соответствующему сеансу, используя в качестве па­ раметра структуру ConnectionSettings, полученную с помощью обработ­ чика подключения.

Добавление голосового чата Добавление комментариев к игре в режиме текстовых сообщений яв­ ляется достаточно распространенным приемом. Тем не менее, даже при очень большой скорости набивания текста это не совсем удобно. Таким образом, возникает необходимость в голосовом способе передачи сооб­ щений, позволяющем не отрывать игрока от процесса игры. Добавление голосового чата в приложение сервер-клиент не представ­ ляет огромного труда. Возьмем пример из главы 18, где мы описывали Р2Р-соединение, и попробуем добавить голосовое общение. Подготовив предварительно наш новый проект, мы должны добавить ссылку на DirectSound, а также на соответствующие переменные и ди­ рективы пространства имен Voice (учитывая, что данное пространство используется и в классе Server, и в классе Client):

Глава 20. Особенности более совершенного использования сетей using Voice = Microsoft.DirectX.DirectPlay.Voice;

using Microsoft.DirectX.DirectSound;

Объявляем переменные Voice для сервера и клиента. Для Р2Р-сети главный компьютер (хост) играет роль сервера, но поскольку любой из объектов Р2Р-сети может принять полномочия хоста, объект peer должен содержать обе части Voice — и для сервера, и для клиента:

private Voice.Client voiceClient = null;

private Voice.Server voiceServer = null;

Pages:     | 1 |   ...   | 3 | 4 || 6 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.