WWW.DISSERS.RU

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

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

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

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

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

Глава 14. Добавление звука Название Описание Distortion Эффект искажения. Осуществляется добавлением гармоник к сигналу, при увеличении уровня изменяется форма волны Echo Эффект эхо, повторяет многократно первоначальный звук с заданной задержкой и спадом амплитуды последующего сигнала Environment Reverberation. Эффект отражения или раскаты. Осуществляется спецификацией 3D Audio Level 2 (I3DL2) Flange Эффект гребня, напоминает эффект хора, плюс эффект эхо только с маленькой задержкой и сменой тона через какое то время Gargle Просто модулирует амплитуду сигнала Parametric Equalizer Этот эффект действует как эквалайзер, позволяя усиливать или подавлять сигналы определенной частоты Waves Reverberation Этот эффект предназначен для использования вместе с музыкой, напоминает эффект раскатов Теперь мы можем выбрать и применить любой из эффектов, выпол­ нив предварительно соответствующие настройки. Вы можете делать это для вторичного буфера с помощью метода GetEffects. Чтобы лучше слы­ шать изменения при установке, необходимо отключить остальные эф­ фекты, за исключением эха. Добавьте следующую секцию кода сразу после вызова SetEffects:

EchoEffect echo = (EchoEffect)sound.GetEffects(0);

EffectsEcho param = echo.AllParameters;

param.Feedback = 1.Of;

param.LeftDelay = 1060.2f;

param.RightDelay = 1595.3f;

param.PanDelay = 1;

echo.AllParameters = param;

Здесь мы используем метод GetEffects для создания объекта EchoEffect, определяющего все изменяемые параметры используемого эффекта эха. Теперь проиграв звук с изменениями, выполненными в дан­ ном методе, и без них, мы должны сразу почувствовать разницу. Таким образом, мы можем управлять любым загружаемым эффектом.

10 Зак. 290 Часть IV. Звук и устройства ввода ИЗМЕНЕНИЕ СВОЙСТВ ЭФФЕКТА Даже при том, что вызов SetEffects может осуществляться только когда буфер остановлен, вы можете изменять параметры загружен­ ных эффектов в реальном времени, даже при проигрывающем бу­ фере.

Краткие выводы В этой главе были рассмотрены следующие вопросы.

• Загрузка и проигрывание статических звуков.

Проигрывание звуков в 3D пространстве.

• Проигрывание звуков с эффектами.

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

Глава 15. Управление устройствами ввода Глава 15. Управление устройствами ввода К настоящему моменту мы охватили концепции построения 3D-rpa фики и рассмотрели вопросы использования звука.

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

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

Управление с клавиатуры.

Управление с помощью мыши.

Управление с помощью джойстика и игровой клавиатуры.

• Работа с обратной связью.

Обнаружение устройств В первую очередь, чтобы использовать код, который будем обсуждать далее в этой главе, мы должны обеспечить необходимые ссылки на Directlnput. Нам также необходимо добавить ссылку на Micro soft.DirectX.Directlnput и директиву using для этого пространства имен.

Даже если ваш компьютер является автономной системой, в нем име­ ется по крайней мере два устройства ввода данных: клавиатура и мышь.

Дополняя машину различными USB-устройствами, ставшими доста­ точно распространенными в настоящее время, мы можем иметь несколь­ ко устройств ввода данных, которые мы должны уметь обнаруживать и.распознавать. Если вспомнить, в начале книги мы говорили о классе Manager, который входит в Direct3D. Приложение Directlnput имеет по­ добный класс, который мы будем использовать для этих задач.

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

Для этой схемы в приложении необходимо добавить некоторые кон­ станты ключевых имен:

private const string AllItemsNode = "All Items";

private const string KeyboardsNode = "All Keyboard Items";

private const string MiceNode = "All Mice Items";

292 Часть IV. Звук и устройства ввода private const string GamePadNode = "All Joysticks and Gamepad Items";

private const string FeedbackNode = "All ForceFeedback Items";

Опираясь на эти константы, необходимо создать пять различных уз­ лов или папок в дереве разветвленной схемы: одна папка для всех уст­ ройств вашей системы, затем по одной секции для мыши, клавиатур, джойстиков и элементов обратной связи. Элементы, находящиеся в пап­ ке «All Items», должны быть продублированы в остальных папках.

Вначале необходимо заполнить папку «All Items». Для этого мы со­ здадим функцию LoadDevices, целью которой является заполнение раз­ ветвленной схемы элементами, находящимися в системе. Добавьте ме­ тод, приведенный в листинге 15.1.

Листинг 15.1. Добавление устройств к разветвленной схеме.

public void LoadDevices() { TreeNode allNodes = new TreeNode(AllItemsNode);

// First get all devices foreach(DeviceInstance di in Manager.Devices) { TreeNode newNode = new TreeNode (string. Format)" (01 - (1} ({2})\ di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid));

allNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(allNodes);

} Как вы можете видеть, класс DeviceList (производный от класса Devices) возвращает список структур Devicelnstance. Эта структура со­ держит всю полезную информацию относительно устройств, включая идентификатор GUID (использующийся при создании устройства), на­ звание продукта и тип устройства.

Для проверки наличия устройства вы можете использовать и другие методы класса Manager. Вполне возможно иметь в системе и виртуаль­ ное устройство «available», которое, в принципе, поддерживается, но на данный момент отсутствует в системе.

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

Теперь предположим, что мы хотели найти лишь некоторые типы ус­ тройств, например, только клавиатуру? Добавьте код листинга 15.2 в конце метода LoadDevices.

Глава 15. Управление устройствами ввода Листинг 15.2. Добавление клавиатуры к разветвленной схеме.

// Now get all keyboards TreeNode kbdNodes = new TreeNode(KeyboardsNode);

foreach(Device!nstance di in Manager.GetDevices(DeviceClass.Keyboard, EnumDevicesFlags.AttachedOnly)) { TreeNode newNode = new TreeNode(string.Format)"{0} - {1} ({2})", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid));

kbdNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(kbdNodes);

Здесь используется новый метод GetDevices класса Manager (вместо знакомого нам Devices), который позволяет определять типы перечис­ ленных устройств. В данном примере мы пытаемся обнаружить объекты keyboards, более того, мы хотим найти только те устройства, которые не­ посредственно подсоединены к системе. Для определения устройств дру­ гих типов код остается таким же, только указываются соответствующие значение используемого класса:

// Now get all mice TreeNode miceNodes = new TreeNode(MiceNode);

foreach(DeviceInstance di in Manager.GetDevices(DeviceClass.Pointer, EnumDevicesFlags.AttachedOnly)) { TreeNode newNode = new TreeNode(string.Format("{0} - {1} ({2})", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid)),• miceNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(miceNodes);

// Now get all joysticks and gamepads TreeNode gpdNodes = new TreeNode(GamePadNode);

foreach(DeviceInstance di in Manager.GetDevices(DeviceClass.GameControl, EnumDevicesFlags.AUDevices)) { TreeNode newNode = new TreeNode(string.Format("(0} - (1} ((21)", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid));

gpdNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(gpdNodes);

294 Часть IV. Звук и устройства ввода Обратите внимание, что указатель устройства мыши в классе имеет тип Pointer. Данный класс не подразумевает использование только для устройства мыши, это более общий тип. Экранные указатели Screen pointers, например, относятся к этой же категории.

Теперь мы попробуем определить или найти в системе устройства с обратной связью. Оно не относится к какому-либо определенному типу устройств, это скорее характеристика или особенность, которую мы хо­ тим поддерживать.

Эта проверка также весьма проста:

// Now get all Force Feedback items TreeNode ffNodes = new TreeNode(FeedbackNode);

foreach(DeviceInstance di in Manager.GetDe?ices(DeviceClass.All, EnumDevicesFlags.ForceFeeback)) { TreeNode newNode = new TreeNode(string.Format("{0} - {1} ({2})", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid)) ;

ffNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(ffNodes);

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

ИСПОЛЬЗОВАНИЕ ПАНЕЛИ УПРАВЛЕНИЯ В классе Manager имеется метод RunControlPanel, который откры­ вает панель управления. Панель управления позволяет установить доступ к настройкам имеющихся в системе устройств.

Использование клавиатуры Каждое из устройств, использующихся в Directlnput, имеет свой соб­ ственный объект Device, подобно тому, как графическая или звуковая карта имеют свои объекты устройств. Количество устройств ввода в Directlnput может быть значительно больше, чем в графических или зву­ ковых картах.

Создание устройства требует наличия идентификатора GUID. Это может быть просто компонент объекта SystemGuid, если мы хотим со­ здать клавиатуру или устройство мыши по умолчанию. Устройства, ко­ торые создаются при этом, достаточно универсальны и позволяют извле­ кать данные из любого устройства, поддерживаемого приложением Directlnput.

Глава 15. Управление устройствами ввода Первое устройство, с которого мы начнем, — клавиатура. Во-первых, нам понадобится переменная устройства:

private Device device = null;

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

private bool running = true;

public void Initializelnput() { // Create our keyboard device device = new Device(SystemGuid.Keyboard);

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

device.Acquire();

while(running) { UpdatelnputState();

Application.DoEvents();

} } Как вы видите, это — полный цикл ввода, вначале которого, исполь­ зуя стандартный модификатор GUID, создается устройство клавиатуры.

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

Таблица 15.1. Уровни совместного доступа в Directlnput Флажок Описание Background Фоновый доступ. Устройство может использоваться на заднем плане или может быть вызвано в любое время, даже если связанное окно не является активным Foreground Активный доступ. Устройство может использоваться только при активном окне, в противном случае, не может быть вызвано 296 Часть IV. Звук и устройства ввода Флажок Описание Exclusive Эксклюзивный режим. Устройство имеет статус монопольного доступа из приложения. Кроме него, никакое другое приложение не может иметь такой же статус доступа, однако неэксклюзивные запросы возможны. Из соображений безопасности на некоторых устройствах исключено совместное использование флажков exclusive и background, например, у клавиатуры и мыши NonExclusive Совместный режим. Устройство может быть распределено по многим приложениям и не требует монопольного доступа.

NoWindowsKey Отключает клавишу «windows key» Для данного приложения мы можем использовать флажки foreground и non-exclusive. После входа в цикл при выполнении приложения мы пе­ реписываем состояние ввода устройства InputState и вызываем обработ­ чик событий DoEvents. Прежде чем записать код метода UpdatelnputState, необходимо создать текстовое поле с атрибутами мультистроки и «толь­ ко для чтения» и установить для него свойство Dock в значении «Fill».

Теперь мы можем добавить следующий метод для изменения текстового окна textbox:

private void UpdatelnputState() { // Check the keys currently pressed first string pressedKeys = "Using GetPressedKeys(): \r\n";

foreach(Key k in device.GetPressedKeysf)) pressedKeys += k.ToString() + " ";

textBoxl.Text = pressedKeys;

} Перед запуском этого приложения осталось сделать две вещи. Следу­ ет обратить внимание на то, что цикл будет выполняться до тех пор, пока переменная running не получит значение «false», поэтому, если мы хо­ тим, чтобы выполнение приложения закачивалось при закрытии формы, необходимо присвоить переменной running значение «false»:

protected override void OnClosed(EventArgs e) { running = false;

} Это может помочь, когда мы вызываем метод инициализации Initialize Input. Изменим основной метод следующим образом:

Глава 15. Управление устройствами ввода static void Main() ( using (Forml frm = new FormlO) ( frm.Show();

frm.InitializelnputO ;

I } Теперь мы можем запустить это приложение. Нажимая и удерживая клавишу в любой момент времени, мы окажемся в текстовом поле. Обра­ тите внимание, что мы уже определили необходимые клавиши, не прибе­ гая к дополнительному коду.

УДЕРЖИВАНИЕ НЕСКОЛЬКИХ КЛАВИШ Большинство клавиатур могут поддерживать нажатие до пяти кла­ виш одновременно. Нажатие большего числа клавиш приведет к иг­ норированию операций.

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

Создайте новый метод инициализации ввода, листинг 15.3.

Листинг 15.3. Метод инициализация для Directlnput и Second Thread.

private System.Threading.AutoResetEvent deviceUpdated;

private System.Threading.ManualResetEvent appShutdown;

public void InitializelnputWithThreadO { // Create our keyboard device device = new Device(SystemGuid.Keyboard);

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

deviceUpdated = new System.Threading.AutoResetEvent(false) ;

appShutdown = new System.Threading.ManualResetEvent(false);

device.SetEventNotification(deviceUpdated);

System.Threading.Thread threadLoop = new System.Threading.Thread) new System.Threading.ThreadStart(this.ThreadFunction));

threadLoop.Start ();

device.Acquired ;

} Часть IV. Звук и устройства ввода Основная предпосылка этого метода такая же, как и в предыдущем, толь­ ко здесь мы объявили две переменные обработчика событий. Одна из них — AutoResetEvent — для Directlnput, другая — ManualResetEvent, чтобы уве­ домить трэд, когда приложение закрывается. Нам также понадобится об­ работчик ThreadFunction, отслеживающий одно из этих событий:

private void ThreadFunction() { System.Threading.WaitHandlef] handles = { deviceUpdated, appShutdown };

// Continue running this thread until the app has closed while(true) { int index = System.Threading.WaitHandle.WaitAny(handles);

if (index == 0) { UpdatelnputState();

} else if (index == 1) { return;

} } } Эта функция является паритетной, когда мы имеем дело с многопоточ­ ными приложениями. В случае если устройство Directlnput сообщает нам о событии изменения состояния устройства, мы вызываем метод UpdatelnputState;

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

protected override void OnClosed(EventArgs e) { if (appShutdown != null) appShutdown.Set();

} И мы должны модифицировать нашу основную процедуру, чтобы вызвать новый метод инициализации:

static void Main() { using (Forml frm = new Form()) Глава 15. Управление устройствами ввода frm.Show() ;

frm.InitializelnputWithThread();

Application.Run(frm);

} } В большинстве случаев нет необходимости получать список всех на­ жатых в данный момент клавиш;

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

Например, чтобы увидеть, была ли нажата клавиша выхода «ESC», мож­ но сделать следующее:

KeyboardState state = device.GetCurrent KeyboardState();

if (state[Key.Escape]) { /* Escape was pressed */ } ОТКРЫТИЕ ПАНЕЛИ УПРАВЛЕНИЯ, ЗАВИСЯЩЕЙ ОТ КОНКРЕТНОГО УСТРОЙСТВА В классе Device имеется метод RunControlPanel, позволяющий откры­ вать панель управления в зависимости от выбранного устройства. На­ пример, при создании клавиатуры этот метод откроет панель со свой­ ствами клавиатуры;

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

Использование устройства мыши Все устройства Directlnput используют один и тот же класс устройств Devices, так что различия между использованием мыши и клавиатуры весьма незначительные. Необходимо переписать метод создания устрой­ ства в коде Initializelnput(), чтобы использовать GUID идентификатор мыши вместо клавиатуры:

device = new Device(SystemGuid.Mouse);

ЭКСКЛЮЗИВНОЕ ИСПОЛЬЗОВАНИЕ МЫШИ При работе с мышью в эксклюзивном режиме в некоторых случаях мы можем полностью скрыть курсор мыши. Иногда это востребова­ но, но как правило, это не желательный эффект. Например, если вы пишете систему меню для вашей игры, которая работает в полно Часть IV. Звук и устройства ввода экранном режиме, пользователь может использовать это меню толь­ ко при помощи мыши. Необходимо предусмотреть для этого слу­ чая отдельные курсоры.

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

Перепишите метод UpdatelnputState следующим образом:

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;

} Здесь мы используем метод перебора, рассмотренный в начале этой главы, для того, чтобы найти подсоединенный джойстик или игровую 302 Часть 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, 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.

304 Часть 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, 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 в наш метод Update InputState.

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

306 Часть IV. Звук и устройства ввода Краткие выводы В этой главе мы рассмотрели.

Управление с клавиатуры.

Управление с помощью мыши.

Управление с помощью джойстика и игровой клавиатуры.

Устройства обратной связи Force Feedback.

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

ЧАСТЬ V 2D ГРАФИКА Глава 16. Приложение Direct3D для 2D-графики Глава 17. Использование DirectDraw для рендеринга 2D-графики 308 Часть 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», так как уст­ ройство будет работать в полноэкранном режиме. Для отображения в полноэкранном режиме мы будем применять вторичный буфер, поэтому ширина и высота вторичного буфера должны быть установлены в соот­ ветствии с заявленными константами размера экрана.

ОБРАБОТКА СОБЫТИЙ ОТКАЗОВ ПРИ СОЗДАНИИ УСТРОЙСТВА Предположим (хотя это маловероятно), что при создании устрой­ ства произойдет отказ и, несмотря на то, что формат устройства перед созданием был проверен, вторичный буфер не будет создан.

310 Часть 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);

} 312 Часть V. 2D графика 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;

} Часть V. 2D графика // See if we're too high or too the left if (position.X < 0) { xUpdate *= -1;

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

} В рассмотренном методе изменение положения спрайта определяется двумя отдельными переменными скорости.

Затем проверяется местоположение спрайта относительно границ эк­ рана, и, если спрайт коснулся границы экрана, направление перемеще­ ния изменяется на противоположное, и объект как бы отражается от края экрана.

Для вызова этого метода добавьте следующий код в начале процеду­ ры 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);

316 Часть V. 2D графика row = rnd.Next(NumberSpritesRow);

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

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

} Позиция спрайта на экране задается следующим образом. Определя­ ется разность между шириной и высотой экрана (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:

Часть V. 2D графика foreach(GraphicsSprite gs in ar) gs. Updated;

Последнее, что мы должны сделать, изменить метод рисования 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 (непрозрачный) и AllPaintingln WmPaint, подобно тому, как это делалось для З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 Зак. 322 Часть V. 2D графика // which is what the CoiorKey struct is initialized to.

ColorKey ck = new CoiorKey();

sprite. SetColorKey (ColorKeyFlags.SourceDraw, ck);

} Как вы можете видеть, этот метод несколько более сложен, чем вариант для Direct3D. После создания устройства DirectDraw мы вызываем проце­ дуру установки уровней доступа к различным ресурсам платы SetCoopera tiveLevel. Поскольку для 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;

324 Часть V. 2D графика private float yUpdate = 1.4 f;

///

/// Constructor for our sprite /// /// Initial x position /// Initial у position public GraphicsSprite(int posx, int posy) { xPosition = posx;

yPosition = posy;

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

yUpdate += (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 нет весь различия. Однако, в случае программной никакого жении, используя спрайт в качестве исходного 10 % быстрее, чем реализации метод DrawFast приблизительно на материала. Исполь­ зуемые флажки сообщают DirectDraw, что он «не должен ожидать» окон­ метод Draw, правда, ценой надежности и устойчивости. Метод Draw чания процедуры рисования, и что для включения параметра прозрачно­ является более гибким и позволяет реализовать различные опера­ сти необходимо использовать цветовой ключ (который был установлен ции по отношению к отображаемым спрайтам. Для простых же опе­ при создании спрайта).

раций рисования целесообразно использовать метод DrawFast.

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

326 Часть 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 const int NumberSpritesRow = 6;

private const int NumberSpritesCol = 5;

private const int SpriteSizeWidth = 50;

private const int SpriteSizeHeight = 45;

Исходный текст программы, включенной в CD диск, использует файл sprites.bmp.

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

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

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

328 Часть 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) 330 Часть V. 2D графика gs. Updated;

backBuffer.ColorFill(O);

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

primary.Flip(backBuffer, FlipFlags.DoNotWait);

this. Invalidated;

} ' I ' ni ^Fi i • л й и л п л ! » жгт т mi т и п г тттч"Ч «пптгт»ттт» иачтут Таким образом, мы видим, что различия между Direct3D и DirectDraw не столь значительные, как можно было предположить вначале.

Краткие выводы В данной главе мы рассмотрели следующие вопросы.

Использование полноэкранного режима.

Рендеринг спрайтов.

Рендеринг анимированных спрайтов.

В следующей главе мы рассмотрим добавление сетевых возможнос­ тей, в частности, работу одноранговых сетей.

ЧАСТЬ 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 В дальнейшем, при написании приложений DirectPlay мы будем иметь дело только с TCP/IP протоколом. Но это вовсе не означает, что мы не можем использовать другие протоколы, поэтому записанный код может быть применим и к другим перечисленным выше протоколам и соедине­ ниям. -"^ ИСПОЛЬЗОВАНИЕ «URL» СТРОКИ В КАЧЕСТВЕ АДРЕСА Адреса могут быть также определены в форме указателя URL, как и все Web-адреса, например, http://www.mycompany.com. Первая секция URL указывает на тип протокола (в данном случае протокол http). Web-страницы, которые мы обычно видим, имеют формат язы­ ка HTML. Ниже приводится пример использования указателя URL для задания адреса в DirectPlay:

x-directplay:/provider=%7BEBFE7BA0-628D-HD2 AE0F-006097B014H%7D;

hostname= www.mygameserver.com;

port= Обратите внимание на тип этого 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 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 Данный флажок отключает проверку параметров этого соединения, позволяя получить выигрыш в быстродействии Часть VI. Добавление сетевых возможностей Флажок Описание DisableLinkTuning Отключает возможность настройки скорости передачи данных в 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 Зак 338 Часть 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. Идентифицирует отдельные экземпляры приложения MaxPlayers Максимальное число пользователей в данном соединении.

Нулевое значение (по умолчанию) устанавливает неограниченное число игроков Число подсоединенных на данный момент пользователей CurrentPlayers Flags Определяют поведение соединения, могут использоваться по отдельности или в различных комбинациях:

• ClientServer • FastSigned • FullSigned • MigrateHost • NoDpnServer • NoEnumerations • RequirePassword SessionName Имя соединения, определенное пользователем Password Пароль для установления соединения. Это значение должно иметь пустой указатель (null) в случае, если флажок RequirePassword не был установлен Теперь, имея структуру описания, мы можем приступить к созданию соединения. Добавьте следующий код в конец метода инициализации:

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 Параметр Описание NoBroadcastFallback Отключает режим ретрансляции данных на серве­ ре. Поддержку возможности передачи данных службой поддержки можно проверить с помощью метода GetSpCaps класса Peer OkToQueryForAddressing Позволяет устройству DirectPlay отображать диалоговые окна для уточнения текущей информации 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. Добавление сетевых возможностей //Do 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 отображении диалогового окна, как можно быстрее получить необходи­ мые нам данные и завершить процедуру.

Наконец, мы готовы начать соединение (вернемся к рассмотрению листинга 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) lb connected = false;

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

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

private delegate void EnableCallback(bool enable);

Теперь попробуем запустить два примера приложения и соединить их друг с другом. В первом примере нажимаем кнопку «Host», при этом дол­ жно появиться сообщение о том^что это приложение будет являться хос Глава 18. Организация сети с равноправными узлами с помощью DirectPlay том соединения. Далее нажимаем кнопку «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 NoComplete Использование этого флажка позволяет не отслеживать событие SendComplete. He используется с флажками NoCopy или Guaranteed 346 Часть VI. Добавление сетевых возможностей Параметр Описание CompleteOnProcess Использование этого флажка позволяет осуществить проверку соединения и отследить событие передачи данных SendComplete. Используется с флажком Guaranteed Guaranteed Посылает сообщение для проверки наличия соединения PriorityHigh Посылает сообщение с высоким приоритетом. Не может использоваться совместно с флажком PriorityLow PriorityLow Посылает сообщение с низким приоритетом. Не может использоваться совместно с флажком PriorityHigh Nonsequential По умолчанию DirectPlay определяет тот же самый порядок приема данных, что и при передаче. Если сообщения достигают компьютера в другом порядке, они будут буферизированы и переупорядочены. При установке данного флажка данные не переупорядочи­ ваются Задание этого флажка позволяет не отслеживать NoLoopBack событие приема данных при пересылке их другому игроку или группе, в которой вы находитесь Использование этого флажка позволяет DirectPlay 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 ется и главное, и подключаемое приложения. Нажатие кнопки «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;

} 348 Часть 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-й порт, а серверы FTP— 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 Часть VI. Добавление сетевых возможностей public class SharedCode { public static readonly Guid ApplicationGuid = new Guid (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, сопровождающийся надписью «поиск соединения».

356 Часть 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." lb 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) });

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 Часть VI. Добавление сетевых возможностей try { 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 362 Часть 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 });

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



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

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