В этом занятии мы расскажем, как с помощью пакета ЛКИ-Creator добавить в вашу игру связь по сети, и продемонстрируем это на реальном примере. Предыдущие статьи и примеры, рассказывающие о том, как быстро запрограммировать игру с помощью ЛКИ-Creator, вы найдете на нашем диске.
То, что вы видите перед собой — результат глобальной переработки сетевой части ЛКИ-Creator. Код компонентов и примера вы найдете на нашем диске.
Сеть средствами DirectPlay
Перед тем как начать разговор о сетевых компонентах ЛКИ-Creator в их новой версии, уделим немножко внимания тому, как работает сеть в DirectPlay. Это, так сказать, теоретические основы — а в следующей главе вы увидите, как они реализованы средствами ЛКИ-Creator.
Перечислим основные этапы создания сетевого приложения на основе DirectPlay:
Инициализация
Начальный шаг при создании любого приложения — инициализация. Есть два способа создания интерфейса IDirectPlay4.
Первый способ заключается в использовании функции DirectPlayCreate, которая создает интерфейс IDirectPlay. Неудобство ее состоит в том, что для получения интерфейса IDirectPlay4 необходимо использовать функцию QueryInterface(), а затем удалить старый интерфейс функцией Release().
Второй метод заключается в использовании функции CoCreateInstance(). При этом вы непосредственно создаете интерфейс IDirectPlay4 без привлечения промежуточных интерфейсов.
Следует заметить, что в обоих методах создания интерфейса IDirectPlay в начале вызывается функция инициализации интерфейса CoInitialize().
Перечисление и инициализация системных провайдеров
Для сетевых соединений DirectPlay предоставляет четыре провайдера: TCP/IP, IPX, модем и прямое соединение. Все они стандартны и присутствуют в любом случае, даже если протокола, который они используют, нет в наличии. Но вполне возможно, что в будущем появятся новые провайдеры, использующие новые протоколы. Для универсальности программы необходимо, чтобы она знала обо всех провайдерах, присутствующих в системе. Это возможно при использовании функций перечисления провайдеров.
DirectPlay перечисляет системные провайдеры при помощи двух функций: IDirectPlay4::DirectPlayEnumerate и IDirectPlay4::EnumConnections(). Эти функции перечисляют все зарегистрированные провайдеры, вне зависимости от того, работают они или нет. Отличие их в том, что первая перечисляет только системные провайдеры, а вторая в дополнение к системным — еще и лобби. К тому же, функция DirectPlayEnumerate перечисляет только провайдеры, а EnumConnection в дополнение к провайдерам — еще и возможные подключения.
Управление сеансом
Сеансом в DirectPlay называют канал связи между двумя или более приложениями. Вполне возможно, что на одном компьютере несколько приложений откроют несколько сеансов.
Открыть сеанс можно разными способами. Например, приложение может выполнить перечисление всех существующих сеансов и присоединяется к одному их них. Другая возможность заключается в создании собственного сеанса.
После того как приложение стало частью сеанса, появляется возможность создавать игроков и обмениваться сообщениями.
Сервер является владельцем сеанса и только он может изменять настройки сеанса.
DirectPlay предоставляет набор функций для управлением сеансом. Рассмотрим их подробнее.
Перечисление сеансов
Функция IDirectPlay4::EnumSession() перечисляет все существующие сеансы.
Функция EnumSessions() имеет два режима работы — синхронный и асинхронный.
По умолчанию EnumSessions() работает в синхронном режиме. При этом перед каждым перечислением функция очищает кэш и выполняет запрос о существовании сеансов. Ответы на запрос принимаются только в период, указанный в параметре dwTimeout. По истечении этого времени все ответы игнорируются. При перечислении сеансов приложение блокируется на время ожидания ответов.
При работе в асинхронном режиме кэш сеансов не очищается, а лишь обновляется. Обновление кэша происходит через промежутки времени, указанные в параметре dwTimeout. При этом происходит удаление сеансов, не отвечающих на запросы, и добавление в кэш новых, только что появившихся. Статус каждого сеанса, находящегося в кэше, будет обновлен.
Открытие сеанса
Открытие сеанса подразумевает под собой как создание нового сеанса, так и подключение к уже существующему. Для открытия сеанса используется функция IDirectPlay4:pen.
После присоединения к сеансу появляется возможность создания игроков и обмена сообщениями. Но следует заметить, что до тех пор, пока игрок не будет создан, приложение не сможет отправлять и принимать сообщения, так как информация об инициаторе действия отсутствует.
Закрытие сеанса
После окончания работы с сеансом его необходимо закрыть. Это делается для того, чтобы игроки получили информацию именно о закрытии соединения, а не об обрыве связи.
Для закрытия сеанса используется функция IDirectPlay4::Close().
При вызове этой функции происходит удаление всех локальных игроков. После этого происходит информирование всех остальных игроков о происшедшем событии при помощи системных сообщений DPMSG_DELETEPLAYERFROMGROUP и DPMSG_DESTROYPLAYERORGROUP.
Управление игроками
Игрок в терминологии DirectPlay представляет собой логический объект, имеющий возможность отправлять и принимать сообщения. Любой сеанс может содержать в себе столько игроков, сколько позволяет пропускная способность сети. Одно приложение может создавать такое количество игроков, какое допускают настройки сеанса.
Каждый игрок при создании получает уникальный идентификационный номер. Данный номер принадлежит ему до тех пор, пока существует сеанс. Если сеанс был прерван и заново открыт, идентификационные номера игроков генерируются повторно.
Все игроки подразделяются на локальные и удаленные в зависимости от того, на локальном или удаленном компьютере они были созданы. Для отправки и приема сообщений компьютер должен иметь хотя бы одного созданного игрока. Также вполне возможно создание нескольких игроков на одном компьютере.
Любое сообщение, отправляемое игроком, предназначается не компьютеру, а конкретному игроку. Сообщение может быть адресовано как удаленному игроку, так и локальному, причем в последнем случае оно не пересылается по сети. Каждое сообщение, полученное приложением, передается игроку с указанием отправителя. При передаче системных сообщений отправитель указывается как DPID_SYSMSG.
Библиотека DirectPlay предоставляет набор функций управления игроками. Эти функции используются для создания игрока, его удаления, перечисления существующих игроков, получения и установки различных данных.
Перечисление существующих игроков
Иногда возникают ситуации, когда необходимо получить список всех игроков, присутствующих в сеансе. Для решения этой задачи библиотека DirectPlay предлагает функцию IDirectPlay4::EnumPlayers(). Данная функция занимается поиском всех пользователей в сеансе. Если в момент ее вызова нет открытых сеансов, возможно перечисление пользователей в удаленном сеансе, для чего необходимо только знать его идентификационный номер — guidInstance. Если удаленный сеанс имеет пароль, перечисление пользователей не представляется возможным. Параметр dwFlags задает флаги, определяющие параметры поиска игроков. Только если игроки удовлетворяют критериям, указанным при помощи этих флагов, будет произведен вызов функции, адрес которой указан в параметре lpEnumPlayersCallback2.
Удаление игрока
Если пользователь желает покинуть сеанс, его игрока необходимо удалить. При этом происходит снятие с очереди получения всех сообщений, адресованных этому игроку, а также исключение его из групп, в которые он входил. Идентификационный номер игрока не освобождается, но и не будет больше использоваться в текущем сеансе.
Удалить игрока может либо то приложение, которое его создало, либо сервер сеанса. Удаление игрока производится при помощи функции IDirectPlay4::DestroyPlayer().
Получение учетной записи игрока
При сеансах с повышенной безопасностью каждый игрок имеет учетную запись, которая необходима для аутентификации. Эта информация может быть получена сервером сеанса при помощи функции GetPlayerAccount().
Получение адреса игрока
Каждый игрок имеет адрес, совпадающий с сетевым адресом компьютера, на котором игрок был создан. В некоторых случаях игрок может иметь несколько адресов, например, если на компьютере установлены сетевая карта и модем.
Получить адрес игрока можно при помощи функции IDirectPlay4::GetPlayerAddress().
Получение и изменение имени игрока
Каждый игрок при создании указывает свое имя. Имя не является уникальным параметром и никак не используется механизмами DirectPlay. Для получения имени игрока применяется функция IDirectPlay4::GetPlayerName().
Получить имя игрока может любой компьютер в сеансе. Изменить же его может только компьютер, на котором этот игрок был создан. Для изменения имени игрока используется функция IDirectPlay4::SetPlayerName().
Сообщения
После того как будет открыт сеанс и созданы игроки, появляется возможность обмена данными между компьютерами посредством сети. Данные, передаваемые по сети между игроками, называются сообщениями.
Существуют сообщения двух типов: сообщения игроков и системные. Сообщения игроков — это данные, которыми игроки или группы обмениваются в процессе сеанса. Системные сообщения являются оповестительными и информируют о произошедших в сеансе изменениях.
Все сообщения, полученные компьютером, помещаются в очередь, из которой приложение производит выборку.
Из очереди сообщения могут быть получены двумя способами. Первый заключается в проверке очереди сообщений в главном цикле приложения. Обычно этот метод используют для однопоточных приложений.
Второй метод состоит в использовании различных потоков для получения сообщения и его обработки.
Системные и пользовательские сообщения
Как уже было сказано, сообщения подразделяются на два типа — пользовательские и системные. Системные сообщения распознаются по полю lpidFrom, которое в этом случае должно быть равно DPID_SYSMSG. Если же сообщение было послано игроком, это поле содержит идентификатор отправителя.
Все сообщения содержат какие-либо данные. В системных сообщениях — это информация о произошедшем событии. Она может быть определена при помощи констант, имеющих префикс DPSYS. Вид данных, представляющих сообщения игроков, определяется только тем приложением, которое отправило эту информацию. Обычно первым элементом этих данных является идентификатор сообщения. Это позволяет проверить очередность получения сообщений. Буфер после приема должен быть приведен к тому же типу данных, который использовался при пересылке сообщения.
Синхронные сообщения
За отправку сообщений отвечают две функции: IDirectPlay4::Send() и IDirectPlay4::SendEx(). Первая из них является более простой в использовании и отправляет сообщения только синхронно. При ее вызове происходит блокировка процессов приложения до тех пор, пока не будет получено подтверждение доставки.
Функция Send() может отправлять сообщения как с гарантией доставки, так и без. Отправка с гарантией доставки требует использования протокола DirectPlay или системного провайдера, поддерживающего такую возможность.
Если сообщение рассылается всем игрокам при помощи флага DPID_ALLPLAYERS, отправитель не получает его копии. Исключение составляет случай, когда указан флаг DPSESSION_NOMESSAGEID, при котором сообщения не имеют заголовка и их владельца невозможно определить (в качестве владельца указывается DPID_UNKNOWN).
Асинхронные сообщения
Возможность асинхронной связи освобождает приложение от блокировки во время отправки каждого сообщения. Также это позволяет приложению использовать мониторинг очереди сообщений и возможность отмены отправки сообщений.
Асинхронные сообщения возможны, только если сеанс использует протокол DirectPlay или при поддержке асинхронных сообщений системным провайдером. Проверить эту возможность системного провайдера можно с помощью функции GetCaps, указав при ее вызове флаг DPCAPS_ASYNCSUPPORTED.
При отправке асинхронного сообщения с использованием функции SendEx() происходит возврат в программу без ожидания уведомления об отправке сообщения и, если указана гарантированная доставка, о его получении.
При успешной доставке функция возвращает DP_OK для синхронных сообщений и DPERR_PENDING — для асинхронных.
Отмена сообщений и использование приоритетов возможны только при использовании асинхронного метода отправки. Причем отменить сообщение можно как при помощи идентификационного номера, генерируемого DirectPlay, так и при помощи приоритетов.
Для отмены сообщения необходимы несколько условий, таких как обязательное использование протокола DirectPlay и наличие отменяемого сообщения в очереди. Если сообщение — групповое и уже получено хотя бы одним членом группы, отменить его нельзя.
Кроме отмены сообщения по его идентификационному номеру, возможна отмена группы сообщений, имеющих определенный приоритет. Эту возможность реализует функция IDirectPlay4::CancelPriority():
Получение сообщений
Все получаемые приложением сообщения помещаются в очередь для последующей выборки. Забрать сообщение из очереди можно при помощи функции IDirectPlay4::Receive(),.
Первый вызов функции Receive() практически всегда приводит к ошибке DPERR_BUFFERTOSMALL, так как мы не знаем необходимого размера буфера для получения сообщения. Повторный вызов функции должен увенчаться успехом, так как перед этим был создан буфер требуемого размера, полученного через параметр lpdwDataSize.
После получения сообщения необходимо проверить, не является ли оно системным. Если параметр lpidFrom равен DPID_SYSMSG, значит, получено системное сообщение. В противном случае это сообщением было отправлено другим игроком.
Получить из очереди сообщения для определенного игрока можно, указав флаг DPRECEIVE_TOPLAYER и поместив в переменную, на которую указывает параметр lpidTo, идентификационный номер получателя. Если же необходимо получить сообщение от определенного игрока, указывается флаг DPRECEIVE_FROMPLAYER, а идентификационный номер игрока помещается в переменную, на которую указывает параметр lpidFrom.
Новые компоненты
Как обычно, первое, что нам нужно сделать — это переустановить пакет ЛКИ-Creator. Как это делается, мы рассказывали не раз — обратитесь к первым двум занятиям с ЛКИ-Сreator. Текст этих статей есть на нашем компакт-диске.
Если вы все сделали правильно, у вас появился в панели LKI новый компонент. Нас пока интересует TLKINet. Он позволяет настроить соединение между пользователями и поддерживает обмен сообщениями. Он никакого визуального эффекта не дают, поэтому, куда на форме его ставить — совершенно неважно. Компонент прелставляет собой надстройку-обертку для работы через интерфейсы DirectPlay с сетевыми ресурсами.
У него есть следующие свойства:
MaxPlayers — максимальное разрешенное количество игроков в сессии.
GUID — глобальный уникальный номер игрока
Async — синхроннай или асинхронный метод достави сообщений используется (зависит от используемого провайдера)
ModemSettings — настройки модема (телефонный номер в пуле и т.д.), если используется соединение по модему
TCPIPSettings — настройки TCP/IP (IP, порт и т.д.), если соединение производиться по протоколу TCP/IP.
Компонент также содержит события, которые можно переопределять, срабатывающие при присоединении нового игрока к сессии, отсоединение игрока от сессии, создание сессии, завершение сессии.
Замечание: вы можете не определять параметры сетевого соединения заранее. При открытии сессии появиться диалог, который поможет вам настроить параметры новой сессии или присоединиться к старой, а также настроить параметры игрока-пользователя.
При этом, сначала выбирается сетевой провайдер (Далее для примера предположим, что выбрано соединение через TCP/IP).
Выбор провайдера. |
Затем создается новая сессия или разделяется уже существующая. Для новой сессии задается имя, а также задается имя для нового игрока.
Если же выбран режим соединения с существующей сессией, то нужно задать IP или логическое имя для сервера, который создал сессию.
А затем выбрать из списка найденных сессий нужную и задать имя для нового игрока.
Чтобы создать сессию и приступить к работе используйте метод LKINet.Open.
Программа Chat
Теперь опробуем на практике наши сетевые возможности. Для этой цели сделаем небольшую программу текстового общения — Chat. Она станет впоследствии прообразом программы текстовой ролевой игры — MUD.
Подключение игроков. |
Итак, создаем проект: он будет и клиентом и сервером, все зависит от того кто раньше завел сессию к которой все остальные смогут подключиться. Примеры эти лежат в каталоге «Клиент и сервер Chat» раздела «Игра своими руками» нашего диска, распакуйте их, как обычно.
Большинство современных стратегий отлично совмещает роль и сервера и клиента, и одна и та же программа по необходимости может служить тем и другим. Когда вы собираетесь поиграть по локальной сетке, скажем, в Heroes of Might & Magic, происходит именно это.
На главной форме стоят: TLKINet, текстовое окно, в котором идут сообщения от сервера, и строка ввода. (это совершенно необязательно, просто для удобства).
После создания сессии игрок-сервер сидит и ждет других. Остальные игроки настраивают сетевое соединение и.разделяют сессию сервера.
Если к сессии присоединился новый игрок можно обработать это, переопределив событие OnAddPlayer компонента LKINet. Напишем там пока вот что:
Messages.Lines.Add(Format(' %s entered the room.', [Player.Name]))
Просто заносим в журнал сообщение о прибытии игрока. Впоследствии напишем там более содержательный код.
Если игрок наобщался досыта и вышел — процедура OnDeletePlayer поможет среагировать на это.
Messages.Lines.Add(Format(' %s left the room.', [Player.Name]))
После этого возможен обмен сообщениями между игроками. Причем переданные сообщения получают все игроки сразу — используется широковещательныя рассылка.
Уже описанного вполне достаточно, чтобы два или более игроков могли соединиться (это можно сделать с одной машины) и «поздороваться» между собой, предварительно настроив сетевое соединение.
Список соединившихся клиентов хранится в параметре Players.
Открытие сессииfor i:=0 to LKINet.Players.Count-1 do if LKINet.Players.RemotePlayer then Messages.Lines.Add(Format(' %s is in the room.', [LKINet.Players.Name])) |
Посылка и прием сообщений
Отправка текстовых сообщений производиться командой SendMessage. Предварительно нужно определить пользовательский тип сообщения — TMUDMessage и его максимальный размер — MESS_ML.
Последовательность действий начинается с заполнения полей сообщения, а затем перебираем всех игроков в сессии и каждому пересылаем наше готовое сообщение. Получается небольшой широковещательный шторм.
Отсылка сообщенийMsgSize := SizeOf(TMUDMessage); GetMem(Msg, MsgSize); try Msg.MessageType := MUD_MESSAGE; if (Length(MessageVal.Text)>MESS_ML) then Msg.Len := MESS_ML // если длина сообщения больше максимальной else Msg.Len := Length(MessageVal.Text); StrLCopy(@Msg^.c, PChar(MessageVal.Text), Msg.Len); // the message is sent to all for i:=0 to LKINet.Players.Count -1 do LKINet.SendMessage(LKINet.Players.ID,Msg,MsgSize); MessageVal.Text := '' finally FreeMem(Msg) End |
Для обработки принятых сообщений используется событие OnMessage.
Если пришло новое сообщение, мы сразу знаем, кто его отправитель — Player, содержимое — Data, длину сообщения — DataSize.
Обработчик представляет собой по сути большой переключатель , в зависимости от типа сообщения, производит нужные действия — например ведет и отображает лог взаимодейтвия игроков.
Обработка принятых сообщений//цикл обработки сообщений //селектор сообщений по типу case DXPlayMessageType(Data) of MUD_MESSAGE: begin if (TMUDMessage(Data^).Len<=0) or (TMUDMessage(Data^).Len > MESS_ML) then s := '' else begin SetLength(s, TMUDMessage(Data^).Len); StrLCopy(PChar(s), @TMUDMessage(Data^).c, Length(s)) end; Messages.Lines.Add(Format('%s> %s', [Player.Name, s])) end else {если тип сообщения неизвестен} End |
Собственно говоря, это все, что необходимо для создания сетевого режима вашей игры. Остальное — «соль и перец по вкусу».