DirectX & Visual Basic

Основы DirectPlay

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

Обзор DirectPlay

DirectPlay - это компонент DirectX, с помощью которого Вы можете создавать сетевые приложения, например, сетевые игры. В DirectPlay есть два вида сетевых подключений: peer-to-peer и client-server.

Так схематически выглядит peer-to-peer игра:

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

Так выглядит схема client-server игры:

Здесь компьютеры игроков подсоединены к серверу. Сервер выполняет следующие функции:

Далее будет говориться о peer-to-peer подключении.

Установление peer-to-peer соединения

В библиотеке DirectX 8 for visual Basic Type Library для peer-to-peer игр существует объект DirectPlay8Peer. Он инициализируется следующим образом:

'Public dx As New DirectX8
'Public dpp As DirectPlay8Peer
Set dpp = dx.DirectPlayPeerCreate

После того, как объект DirectPlay8Peer был создан, необходимо установить объект, который будет реализовывать события DirectPlay. Для этого нужно вставить в раздел объявлений этого объекта (например, формы) следующую строку:

Implements DirectPlay8Event

После этого нужно написать реализации методов класса DirectPlay8Event. У него есть следующие события:

Private Sub DirectPlay8Event_AddRemovePlayerGroup(ByVal lMsgID As Long, ByVal lPlayerID As Long, ByVal lGroupID As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_AppDesc(fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_AsyncOpComplete(dpnotify As DxVBLibA.DPNMSG_ASYNC_OP_COMPLETE, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_ConnectComplete(dpnotify As DxVBLibA.DPNMSG_CONNECT_COMPLETE, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_CreateGroup(ByVal lGroupID As Long, ByVal lOwnerID As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_CreatePlayer(ByVal lPlayerID As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_DestroyGroup(ByVal lGroupID As Long, ByVal lReason As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_DestroyPlayer(ByVal lPlayerID As Long, ByVal lReason As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_EnumHostsQuery(dpnotify As DxVBLibA.DPNMSG_ENUM_HOSTS_QUERY, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_EnumHostsResponse(dpnotify As DxVBLibA.DPNMSG_ENUM_HOSTS_RESPONSE, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_HostMigrate(ByVal lNewHostID As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_IndicateConnect(dpnotify As DxVBLibA.DPNMSG_INDICATE_CONNECT, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_IndicatedConnectAborted(fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_InfoNotify(ByVal lMsgID As Long, ByVal lNotifyID As Long, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_Receive(dpnotify As DxVBLibA.DPNMSG_RECEIVE, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_SendComplete(dpnotify As DxVBLibA.DPNMSG_SEND_COMPLETE, fRejectMsg As Boolean)
End Sub

Private Sub DirectPlay8Event_TerminateSession(dpnotify As DxVBLibA.DPNMSG_TERMINATE_SESSION, fRejectMsg As Boolean)
End Sub

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

После этого можно зарегистрировать объект - обработчик событий, как показано ниже:

'frmConnect - объект класса Form,
'реализующий методы класса DirectPlay8Event
dpp.RegisterMessageHandler frmConnect

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

Следующее, что надо сделать - выбрать способ сетевого подключения. DirectPlay поддерживает соединения по протоколу TCP/IP, по модему, с использованием последовательного порта. Метод GetCountServiceProviders объекта DirectPlay8Peer возвращает количество возможных способов сетевых подключений, а метод GetServiceProvider - информацию о конкретном способе подключения. Ниже приведён пример кода, который выводит названия всех способов подключения в элемент управления ListBox.

Dim i As Integer
Dim dpnSPInfo As DPN_SERVICE_PROVIDER_INFO

For i = 1 To dpp.GetCountServiceProviders
 dpnSPInfo = dpp.GetServiceProvider(i)
 List1.AddItem dpnSPInfo.Name
Next

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

Set dpAddres = dx.DirectPlayAddressCreate
dpAddres.SetSP dpp.GetServiceProvider(List1.ListIndex + 1).Guid

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

Создание новой сессии

Создание новой сессии может осуществляться, например, такой процедурой:

Public Sub CreateHost()
bHost = True
Dim pInfo As DPN_PLAYER_INFO
Dim AppDesc As DPN_APPLICATION_DESC
With AppDesc
    .guidApplication = AppGUID 'Уникальный идентификатор приложения
    .lMaxPlayers = iMaxPlayers 'Максимальное количество игроков в сессии
    .SessionName = sGameName 'Название сессии
    If bHostMigrate Then
     'Хост может меняться в процессе игры
     .lFlags = .lFlags Or DPNSESSION_MIGRATE_HOST
    End If
End With
pInfo.Name = sUserName
pInfo.lInfoFlags = DPNINFO_NAME
dpp.SetPeerInfo pInfo, DPNOP_SYNC
dpp.Host AppDesc, dpAddres, DPNHOST_OKTOQUERYFORADDRESSING
Set dpAddres = Nothing
End Sub

GUID используется для того, чтобы подключающиеся друг к другу приложения были одинаковыми. GUID - это строка, которая представляет из себя что-то вроде "{BC4B94BF-7DCE-45EA-B73D-1249C17385C2}". Для генерации своего собственного GUID удобно пользоваться методом CreateNewGuid объекта DirectX8 (создание случайного GUID).

Подключение к существующей сессии

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

Сначала нужно начать поиск хостов.

Dim Desc As DPN_APPLICATION_DESC
Desc.guidApplication = AppGUID
mEnumAsync = dpp.EnumHosts(Desc, Nothing, dpAddres, INFINITE, 0, INFINITE, DPNENUMHOSTS_OKTOQUERYFORADDRESSING, ByVal 0&, 0)

Закончить поиск можно следующим образом:

If mEnumAsync <>  0 Then dpp.CancelAsyncOperation mEnumAsync, 0

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

Private Sub DirectPlay8Event_EnumHostsResponse(dpNotify As DxVBLibA.DPNMSG_ENUM_HOSTS_RESPONSE, fRejectMsg As Boolean)
AddHostsToListBox dpNotify
End Sub

Информацию из структуры типа DPNMSG_ENUM_HOSTS_RESPONSE можно извлечь, например, так:

'Private Type HostFound
' AppDesc As DPN_APPLICATION_DESC
' AddressHost As String
' AddressDevice As String
' TimeLastFound As Long
' SInfo As String
'End Type
'Dim mHosts() As HostFound

Private Sub AddHostsToListBox(fHost As DPNMSG_ENUM_HOSTS_RESPONSE)
'Здесь мы добавляем хост в список
If Not bSearch Then Exit Sub
Dim i As Integer
Dim Found As Boolean
        
For i = 0 To iHostCount
 If mHosts(i).AppDesc.guidInstance = fHost.ApplicationDescription.guidInstance Then
  'Этот хост уже был найден ранее
  mHosts(i).TimeLastFound = GetTickCount
  Found = True
  With fHost.ApplicationDescription
   mHosts(i).SInfo = .SessionName & " - " & CStr(.lCurrentPlayers) & "/" & CStr(.lMaxPlayers) & " - Latency:" & CStr(fHost.lRoundTripLatencyMS) & " ms"
  End With
  Exit For
 End If
Next
        
If Not Found Then
 ReDim Preserve mHosts(iHostCount + 1)
 mHosts(iHostCount + 1).AppDesc = fHost.ApplicationDescription
 mHosts(iHostCount + 1).AddressHost = fHost.AddressSenderUrl
 mHosts(iHostCount + 1).AddressDevice = fHost.AddressDeviceUrl
 With fHost.ApplicationDescription
  mHosts(iHostCount + 1).SInfo = .SessionName & " - " & CStr(.lCurrentPlayers) & "/" & CStr(.lMaxPlayers) & " - Latency:" & CStr(fHost.lRoundTripLatencyMS) & " ms"
 End With
 'Записываем время, когда хост был найден последний раз
 mHosts(iHostCount + 1).TimeLastFound = GetTickCount
 iHostCount = iHostCount + 1
End If
End Sub

К выбранному хосту подключаемся с помощью процедуры ConnectToHost.

'Private Sub DirectPlay8Event_ConnectComplete(dpNotify As DxVBLibA.DPNMSG_CONNECT_COMPLETE, fRejectMsg As Boolean)
'bGotEvent = True
'If dpNotify.hResultCode = DPNERR_SESSIONFULL Then 'Подсоединилось слишком много людей
'    MsgBox "К этой сессии уже подключилось максимальное количество игроков.  Выберите другую сессию или создайте свою собственную.", vbOKOnly Or vbInformation, App.Title
'Else
'    bConnectComplete = True
'End If
'End Sub

Private Sub ConnectToHost(hHost As HostFound)
Dim HostAddr As DirectPlay8Address
Dim DeviceAddr As DirectPlay8Address
    
Dim dpApp As DPN_APPLICATION_DESC
     
'Подсоединяемся к игре
Dim pInfo As DPN_PLAYER_INFO
'Устанавливаем информацию о нашем игроке
pInfo.Name = sUserName
pInfo.lInfoFlags = DPNINFO_NAME
dpp.SetPeerInfo pInfo, DPNOP_SYNC

With hHost.AppDesc
 dpApp.guidApplication = .guidApplication
 dpApp.guidInstance = .guidInstance
 iMaxPlayers = .lMaxPlayers
End With
    
bGotEvent = False
bConnectComplete = False
'Получаем адрес хоста
If hHost.AddressHost <> vbNullString Then
 Set HostAddr = dx.DirectPlayAddressCreate
 HostAddr.BuildFromURL hHost.AddressHost
Else
 Set HostAddr = dpAddres
End If
If mHosts(List2.ItemData(List2.ListIndex)).AddressDevice <> vbNullString Then
 Set DeviceAddr = dx.DirectPlayAddressCreate
 DeviceAddr.BuildFromURL hHost.AddressDevice
Else
 Set DeviceAddr = dpAddres
End If
 dpp.Connect dpApp, HostAddr, DeviceAddr, DPNCONNECT_OKTOQUERYFORADDRESSING, ByVal 0&, 0
Do While Not bGotEvent 'ждём, пока не произойдёт событие СonnectСomplete
 DoSleep 5 'Ждём 5 милисекунд
Loop
If bConnectComplete Then
 'Мы подсоединились к игре
 bHost = False
 'Очищаем адреса
 Set HostAddr = Nothing
 Set DeviceAddr = Nothing
 Set dpAddres = Nothing
 dpp.UnRegisterMessageHandler
 frmChat.Show
 Unload Me
End If

End Sub

Обмен сообщениями

Теперь, когда соединение установлено, всё просто - остаётся только отправлять и получать сообщения :-) .

Послать сообщение можно при помощи метода SendTo объекта DirectPlay8Peer. Первый параметр этого метода - число типа Long, определяющее какому игроку мы посылаем сообщение, можно воспользоваться константой DPNID_ALL_PLAYERS_GROUP, чтобы послать сообщение всем игрокам, или послать сообщение конкретному игроку (его ID можно узнать в процедуре DirectPlay8Event_CreatePlayer); второй параметр - массив байтов, который содержит само сообщение; третий параметр - приоритет сообщения; четвёртый параметр - задержка в милисекундах перед отправкой сообщения; пятый параметр - совокупность флагов из набора констант CONST_DPNSENDFLAGS. Сам метод является функцией и возвращает асинхронный идентификатор данной операции (тип Long). Этот идентификатор можно использовать в методе CancelAsyncOperation, чтобы остановить данную операцию, если она совершается асинхронно.

Для заполнения буфера с сообщением можно пользоваться процедурами AddDataToBuffer и AddStringToBuffer.

Sub AddDataToBuffer(Buffer() As Byte, lData As Any, lSize As Long, lOffset As Long)
'lData - переменная, которую мы записываем в буфер
'lSize - размер предыдущей переменной в байтах
'lOffset - позиция, начиная с которой в буфере, мы будем записывать данные
Sub AddStringToBuffer(Buffer() As Byte, StringData As String, lOffset As Long)
'StringData - строка, помещаемая в буфер

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

'Const MSGChat As Long = 0
Public Sub SendText(sMessage As String)
Dim lMsg As Long, lOffset As Long
Dim oBuf() As Byte
'Посылаем это сообщение всем
lMsg = MSGChat
lOffset = NewBuffer(oBuf)
AddDataToBuffer oBuf, lMsg, LenB(lMsg), lOffset
AddStringToBuffer oBuf, sMessage, lOffset
dpp.SendTo DPNID_ALL_PLAYERS_GROUP, oBuf, 0, DPNSEND_NOLOOPBACK
End Sub

Послать файл не намного сложнее. Это будет делать процедура SendFile.

'Const MSGFile As Long = 1
Public Sub SendFile(sFileName As String)
Dim lMsg As Long, lOffset As Long, lFileSize As Long
Dim oBuf() As Byte
Dim FileData() As Byte
'Посылаем этот файл всем
lMsg = MSGFile
lOffset = NewBuffer(oBuf)
lFileSize = FileLen(sFileName)
ReDim FileData(1 To lFileSize)
Open sFileName For Binary As #1
Get #1, 1, FileData 'Читаем данные с диска в массив FileData
Close #1
AddDataToBuffer oBuf, lMsg, LenB(lMsg), lOffset
'Записываем в буфер имя файла
AddStringToBuffer oBuf, sFileName, lOffset
'Записываем в буфер длину файла
AddDataToBuffer oBuf, lFileSize, LenB(lFileSize), lOffset
'Записываем в буфер данные из файла
AddDataToBuffer oBuf, FileData(1), lFileSize, lOffset
dpp.SendTo DPNID_ALL_PLAYERS_GROUP, oBuf, 0, DPNSEND_NOLOOPBACK
UpdateText "Вами был отправлен файл " & sFileName
End Sub

Таким образом, по первым четырём байтам сообщения (длина переменной типа Long) мы впоследствии можем определить, что нами было получено - файл или строка.

Чтобы научиться получать сообщения достаточно написать обработчик события Receive.

'Процедура UpdateText просто добавляет свой параметр в TextBox
Private Sub DirectPlay8Event_Receive(dpNotify As DxVBLibA.DPNMSG_RECEIVE, fRejectMsg As Boolean)
Dim sPeer As String
sPeer = dpp.GetPeerInfo(dpNotify.idSender).Name

Dim lMsg As Long, lOffset As Long, lFileSize As Long
Dim dpPeer As DPN_PLAYER_INFO
    
With dpNotify
GetDataFromBuffer .ReceivedData, lMsg, LenB(lMsg), lOffset
Select Case lMsg
Case MSGChat
 Dim sChat As String
 sChat = GetStringFromBuffer(.ReceivedData, lOffset)
 UpdateText "<" & sPeer & ">" & sChat
Case MSGFile
 Dim sFileName As String, iAns As Integer
 Dim FileData() As Byte
 sFileName = GetStringFromBuffer(.ReceivedData, lOffset)
 GetDataFromBuffer .ReceivedData, lFileSize, LenB(lFileSize), lOffset
 iAns = MsgBox(sPeer & " послал Вам файл." & vbCrLf & _
             "Исходное имя файла: " & sFileName & "." & vbCrLf & _
             "Размер файла: " & Str(lFileSize) & " байт." & vbCrLf & _
             "Сохранить файл?", vbYesNo Or vbDefaultButton1 Or vbQuestion, App.Title)
 If iAns = vbYes Then
  CommonDialog1.FileName = vbNullString
  CommonDialog1.flags = cdlOFNHideReadOnly Or cdlOFNOverwritePrompt
  CommonDialog1.ShowSave
  If CommonDialog1.FileName = vbNullString Then Exit Sub
  ReDim FileData(1 To lFileSize)
  GetDataFromBuffer .ReceivedData, FileData(1), lFileSize, lOffset
  Open CommonDialog1.FileName For Binary As #1
  Put #1, 1, FileData
  Close #1
  UpdateText "Вами был получен файл " & sFileName & _
             " и сохранён под именем " & CommonDialog1.FileName
 End If
End Select
End With

End Sub

Как видно, вся информация о полученном сообщении находится в структуре dpNotify. Из неё можно извлечь ID игрока, от которого получено сообщение (а по нему определить его имя). Кроме того, структура содержит буфер ReceivedData, который содержит ровно то, что мы в него помеcтили на этапе отправки сообщения. Извлекать информацию из буфера можно процедурой GetDataFromBuffer, а также функцией GetStringFromBuffer.

Sub GetDataFromBuffer(Buffer() As Byte, lData As Any, lSize As Long, lOffset As Long)
'lData - переменная, которую мы читаем из буфера
'lSize - размер предыдущей переменной в байтах
'lOffset - позиция, начиная с которой в буфере, мы будем читать данные
Function GetStringFromBuffer(Buffer() As Byte, lOffset As Long) As String
'Значение, возвращаемое функцией - строка, считываемая из буфера

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

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

Если у Вас нет локальной сети, то возможно тестировать программу на локальном компьютере. Для этого нужно пользоваться протоколом TCP/IP (необходимо запустить как минимум два экземпляра программы, в одной создать сессию, а в другой - подключиться к ней) и на зарос о вводе адреса удалённого компьютера ввести "127.0.0.1" (без кавычек) - это значит, что все сообщения будут идти от Вашего компьютера к Вашему же компьютеру. Свой ip-адрес в Internet или локальной сети можно узнать, набрав в консоли winipcfg (win98) или ipconfig (winNT/2000/XP). Кроме ip-адресов можно вводить имена компьютеров в локальной сети. После того, как подключение было завершено, для обмена сообщениями достаточно набрать текст в нижнем TextBox'е и нажать Enter.

Hosted by uCoz