BGT Network

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

И так, для начала немного теории.

Реализация работы с сетью в BGT выполнена при помощи двух классов: «network и network_event».
Первый предназначен для инициализации серверной и клиентской части, выполнения подключений к пирам, отправки пакетов данных и несколько других, не столь значительных действий.
Второй же служит для мониторинга сетевых событий, при помощи объектов этого класса, осуществляется получение информации о подключениях и отключениях пиров, а также получение пакетов данных.
Сейчас я перечислю методы и свойства, входящие в эти классы, а ниже разберу каждый более подробно.

  • connect
    Этот метод служит для подключения к серверу, он принимает IP-адрес и порт сервера, и выполняет запрос на подключение.
  • destroy
    Этот метод сбрасывает состояние объекта класса «network» к первоначальному состоянию.
  • Методы disconnect_peer, disconnect_peer_softly и disconnect_peer_forcefully служат для отключения какого либо клиента от сервера, или наоборот, для отключения сервером какого либо клиента.
  • get_peer_address, используется для получения IP-адреса пира, то есть участника сетевого подключения.
  • get_peer_average_round_trip_time, используется для пингования пиров.
  • get_peer_list, позволяет получить список всех подключённых пиров.
  • request, используется для мониторинга сетевых событий, будет более подробно описан ниже.
  • send_reliable и send_unreliable, выполняют отправку пакетов данных между клиентами и сервером.
  • set_bandwidth_limits, устанавливает ограничение на входящий и исходящий трафик.
  • setup_client и setup_server, инициализируют объект класса «network» как клиент или как сервер соответственно.

Свойства класса network:

  • connected_peers, кол-во подключённых в данный момент пиров.
  • bytes_sent и bytes_received, количество отправленных и полученных байт.
  • active, информация о том, является ли данный объект активным, тоесть инициализированным в качестве сервера или клиента.

Класс network_event вообще не имеет методов, он представляет собой пакет свойств, их я перечислю ниже.

  • type, показывает тип события
  • peer_id, идентификатор пира, вызвавшего событие.
  • channel, номер канала, по которому был получен пакет.
  • message, полученный пакет данных.

И так, после краткого знакомства опишу всё более подробно и более понятно.

Для того, чтобы выполнить подключение клиента к серверу, нужно запустить приложение сервера, то есть приложение, в котором объект класса «network» будет инициализирован в качестве сервера.
Затем нужно запустить клиент, он должен инициализировать объект класса «network» в качестве клиента.
После этого клиент должен выполнить подключение к серверу, используя его адрес и порт.
Сервер в свою очередь, должен принимать подключения, мониторя сетевые события.
Сейчас я покажу короткий пример системы клиент/сервер, и попутно постараюсь подробно описать, что я сделал.

Клиент

//Пример клиента.
//Сначала я создаю пиременную id, в ней будет храниться идентификатор подключения к серверу.
int id;
//Затем я создаю два объекта классов network и network_event соответственно. Первый я назову net, а второй event.
network net;
network_event event;
//Теперь открываю главную функцию, и делаю в ней два поля, для получения ip адреса и порта сервера, в первом поле по умолчанию будет написано localhost, то есть адрес вашего компьютера.
void main() {
string ip=input_box("Ip", "Введите IP-адрес сервера.", "localhost");
int port=string_to_number(input_box("Порт", "Введите порт сервера.", 2223));
//Обычно всё отлично работает через localhost, однако если у вас возникнут какие-то проблемы, используйте эквивалентный адрес 127.0.0.1.
//Далее идёт проверка на некорректные данные, если при запуске клиента поле IP-адреса или порта не были заполнены, то клиент просто закроется.
if(ip=="" or port==0)
exit();
//Затем я вызываю метод «setup_client» - это стандартный метод класса «net».
if(!net.setup_client(100, 1)) {
alert("Ошибка.", "Не удалось инициализировать клиент.");
exit();
}
/*
Он принимает два аргумента, первый это число каналов, а второй число максимальных подключений, этот второй аргумент часто запутывает людей, которые только начинают работать с сетью, как не новичок в этом деле хочу сказать, что на практике мне всегда было достаточно одного подключения, так как клиент всегда соединяется лишь с одним сервером.
Максимальное число каналов 100, поэтому первым аргументом я 100 и указываю.
Вторым аргументом, как и написал выше, выступает единица.
Данный метод возвращает «true» в случае успеха, и «false» в случае неудачи, поэтому я сразу делаю вызов этого метода внутри условия, которое проверяет его на «false» и в случае неудачного выполнения, клиент выведет окно с ошибкой и закроется.
Честно сказать, я не знаю ситуаций, при которой метод «setup_client» может вернуть «false», за исключением намеренного указания недопустимых аргументов, но для наглядности я сделал эту проверку.
*/
//Затем происходит открытие окна клиента, и выполняется запрос подключения к серверу.
show_game_window("Client.");
id=net.connect(ip, port); //Метод connect возвращает уникальный идентификатор сервера, который мы будем использовать для отправки данных на сервер.
//Это очередной момент, который путает новичков, метод connect класса network, лишь выполняет запрос подключения к серверу, а не само подключение, однако, как я понял, сам процесс подключения происходит в параллельном потоке, поэтому функция connect сразу возвращает идентификатор сервера, скоторым она, возможно, установит соединение.
//У вас возможно возникает вопрос, как в таком случае выяснить, было ли на самом деле установлено соединение, или же нет, для этого как раз и служит нашь ранее созданый объект класса network_event.
//Создаём таймер прерывания подключения, так как процесс подключения может затянуться, он может занять около 30-40 секунд, прежде чем BGT решит, что сервер недоступен, поэтому мы будем обрывать попытку подключения самостоятельно.
timer connect_timer;
//Запускаем вечный цикл while, и начинаем мониторинг.
while(true) {
/*
Метод request класса network, возвращает объект класса event, который содержит в себе информацию о событие.
Тип события можно получить из свойства type, это свойство имеет тип int и может принимать четыре следующих значения.
для упрощения понимания в BGT существуют константы, которые соответствуют всем состояниям свойства type.
0, константа event_none, событие отсутствует, то есть в данный момент нет никакого события.
1, константа event_connect, в случае клиента это означает, что было установлено соединение с сервером, в случае сервера, что подключился клиент.
2, константа event_receive, был получен пакет данных.
3, константа event_disconnect, в случае клиента означает, что подключение к серверу не удалось или сервер разорвал соединение, в случае сервера, что какой-то пир отключился от сервера.
*/
//И так, для начала получаем событие в объект event, то есть присваиваем объекту event результат метода request.
event=net.request();
//Теперь, делаем проверку на неудачу подключения.
if(event.type==event_disconnect or connect_timer.elapsed>15000) {
alert("Ошибка.", "Не удалось подключиться к серверу.");
exit();
}
//Для проверки используем константу «event_disconnect», а также наш таймер, если request возвращает нам disconnect или с момента запуска таймера прошло 15 секунд, выводится сообщение об ошибке и клиент закрывается.
//Обратите внимание, что при неудачном подключение мы просто закрыли клиент, однако для более крупных проектов, в которых такое поведение может быть неудобным, вы будете делать возврат в главное меню или повторять попытку подключения, и если вы прекратили выполнение мониторинга по таймеру, обязательно переинициализируйте Ваш объект «network», то есть вызовите метод «destroy» и заново вызовите «setup_client» с необходимыми параметрами.
//Это требуется для того, чтобы соединение точно не могло быть установлено тогда, когда мы этого не ждём, это может произойти при плохом интернете, когда мы уже решили, что подождали достаточно, и прекратили мониторинг сети, и вдруг соединение устанавливается, но мы уже не видим этого, так как находимся в главном меню, и потом, когда пользователь снова попытается выполнить подключение, у него либо вообще ничего не получится, либо, если при инициализации клиента в качестве кол-ва подключений было выбрано значение выше единицы, будет установлено ещё одно подключение с тем же сервером, и это грозит более серьёзными проблемами.
//Например, после подключение клиента к серверу, сервер начинает рассылать каждому клиенту информацию о мире, а также об изменениях в нём, например шаги других игроков, а теперь представьте, что ваш клиент поддерживает два одновременных подключения к серверу, и для каждого из этих подключений, сервер отправляет информационные пакеты, помимо удвоенной интенсивности трафика, вас также ждут незабываемые впечатления от раздвоенных звуков или сообщений вашей программы экранного доступа.
//Поэтому будьте очень внимательны при реализации такого прерывания, вы можете конечно вообще не делать его, но иногда это действительно может быть полезным.
//Давайте всё-таки вернёмся обратно к реализации подключения к серверу, следующим шагом мы проверяем успешность подключения.
if(event.type==event_connect) {
alert("Информация.", "Подключение успешно.");
break;
}
//Выводим сообщение об успешном подключение и прерываем цикл. Хочу сразу предупредить, такие функции как «alert, question, input_box и другие функции», которые способны приостановить выполнение вашего скрипта, очень плохо подходят для работы с сетью, так как для поддержания стабильного соединения требуется постоянно мониторить события сети, а выше перечисленные функции не позволяют это сделать, поэтому как можно быстрее рекомендую нажать вам кнопку ОК в появившемся сообщении, чтобы скрипт как можно быстрее перешёл к стадии проверки сетевых событий.
//Кстати, когда мы выполняли вызов метода connect, мы сразу присвоили результат этого вызова переменной id, однако есть ещё один способ получение идентификатора сервера. У объекта класса network_event имеется свойство peer_id, которое содержит в себе идентификатор пира, в нашем случае сервера, с которым связанно данное событие, в нашем случае подключение, поэтому можно воспользоваться этим свойством для получения идентификатора сервера.
}
//Запускаем очередной вечный цикл while.
while(true) {
//Получаем событие.
event=net.request();
//Проверяем отключение от сервера.
if(event.type==event_disconnect) {
alert("Информация.", "Сервер разорвал соединение.");
exit();
}
//Тут можно не торопиться жать ОК, так как дальнейший мониторинг не имеет смысла, после нажатия ОК клиент закроется.
//Также не обязательно выполнять повторную инициализацию объекта network, так как соединение было разорвано и восстановлено само по себе оно точно не будет.
//Проверяем на возможные входящие пакеты.
if(event.type==event_receive)
alert("Информация.", "Получен пакет от сервера: \\r\\n"+event.message+"\\r\\nНа канал: "+event.channel+".");
//Выводим содержимое пакета и номер канала, на который он пришёл, также постарайтесь как можно быстрее закрыть это окно и вернуть скрипт к выполнению проверки сетевых событий.
//Для наглядности примера сделаем отправку пакета данных на сервер.
if(key_pressed(KEY_RETURN))
net.send_unreliable(id, "Hello.", 0);
//По нажатию enter, отправляем пакет "Hello", по каналу 0.
/*

Думаю здесь стоит остановиться и разьобрать эту тему более подробно.

Класс network имеет два метода для отправки пакетов, send_reliable и send_unreliable.
Первый выполняет отправку так называемого надёжного пакета. Отправка таких пакетов более медленная, но более безопасная, в том смысле, что надёжный пакет дойдёт до получателя с большей вероятностью.
Второй же тип пакетов, называется ненадёжные пакеты, они доставляются гораздо быстрее чем надёжные, но есть больше шансов, что такой пакет может вообще не дойти.
Однако как раз этот факт и пугает новичков, ну по крайней мере я так полагаю, и поэтому они стараются всегда использовать надёжные пакеты, что является большой ошибкой.
Как я уже сказал, надёжные пакеты идут дольше чем ненадёжные, и очень частое их использование в конечном итоге приводит к крупным лагам. Да, действительно, все данные доходят, но делают это порой с огромными задержками в секунды, а то и в несколько десятков секунд!
Поэтому для выбора типа пакета, руководствуйтесь логикой, когда какой из них действительно необходим.
Например пакеты, отвечающие за регистрацию и вход на сервер будет лучше сделать надёжными, тогда как отправку новых координат персонажа наоборот, лучше сделать ненадёжными.
Ещё раз повторюсь, не злоупотребляйте надёжными пакетами, так как это приводит к лагам, но совсем отказываться от них тоже не стоит, иногда они действительно нужны, например при отправке сообщений в чат или применения новых настроек игры.

Каналы:

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

Дело в том, что все пакеты, отправленные друг за другом, приходят в том же порядке, в каком они были отправлены, даже если первый отправленный пакет в несколько раз массивнее второго отправленного пакета, получатель сначала получит первый, более большой, и затем уже только маленький пакет.
Это правило действует для одного канала, то есть при использовании нескольких каналов, пакеты не задерживают друг друга, это особенно актуально, когда требуется отправлять надёжные и ненадёжные пакеты.
Так как надёжные пакеты требуют более большого времени доставки, и все пакеты должны приходить по порядку отправки, они будут задерживать все ненадёжные пакеты, которые были посланы вслед за надёжными.
Поэтому не стесняйтесь использовать каналы, максимальное их число 100, и нумерация начинается с нуля, то есть доступен диапазон каналов от 0 до 99.
Количество каналов определяется при инициализации в качестве клиента и сервера, будьте внимательны при установки кол-ва каналов для сервера и клиента, так как если вы поставите на сервере меньшее количество, чем в клиенте, то пакеты, отправленные по каналам, не обрабатываемым сервером просто не дойдут, и наоборот, клиенты не будут получать те пакеты, которые были отправлены сервером по недоступным для клиентов каналам.
Какой канал для каких действий использовать, решать вам, это не имеет значения, имеет значение только то, что вы должны стараться распределять нагрузку между несколькими каналами, например отправлять пакеты для входа на сервер по одному каналу, чат по другому, перемещение персонажей по третьему и т.д.
Теперь, пожалуй, я закончу отступление и продолжу писать клиент.
*/
//Проверяем escape.
if(key_pressed(KEY_ESCAPE)) {
net.destroy();
exit();
}
//Збрасываем состояния нашего объекта network, что автоматически разрывает все активные соединения, в нашем случае одно единственное соединение с сервером, и завершает клиент.
}
}
//Всё, клиент полностью готов, поместите весь данный код в отдельный файл, рекомендую назвать его "client.bgt", и попробуйте запустить, должно появиться поле, в которое предлогается ввести ip адрес сервера, но так как сервер ещё не готов, подключиться у вас не получится, поэтому закройте клиент, сделать это можно нажав Cancel в поле для ввода ip, и последующем нажатии Cancel в поле для порта сервера.

Сервер

//пример сервера.

//Создаём объекты для работы с сетью.
network net;
network_event event;
//Создаём таймер для закрытия сервера, можете конечно убрать этот блок кода, но я думаю так будет удобнее.
timer total_time;
void main() {
//Делаем проверку на неудачную инициализацию, она может не состояться, если функции переданы недопустимые значения, или если указанный порт уже занят другим приложением.
if(!net.setup_server(2223, 100, 4000)) {
alert("Ошибка.", "Не удалось инициализировать сервер.");
exit();
}
//Метод «setup_server» принимает три аргумента, первый это порт, который будет прослушивать сервер, второй это максимальное количество каналов, а третий, максимальное количество подключений.
//Стандартный порт, который мы указали в клиенте, 2223, поэтому первым аргументом я тоже передаю это число.
//Количество каналов, как и в клиенте я делаю максимальным, то есть 100.
//Максимально возможное количество одновременных соединений с сервером, 4000, я также указываю максимум, правда я очень не уверен, что сервер на самом деле сможет выдержать такое кол-во подключений.
//Выводим сообщение об успешном запуске, постарайтесь быстро закрыть его когда оно появится.
alert("Информация.", "Сервер успешно запущен.");
//Запускаем while цикл.
while(true) {
//снятие нагрузки на процессор.
wait(5);
//Проверяем таймер остановки сервера.
if(total_time.elapsed>60000) {
net.destroy();
exit();
}
//Здесь выполняется проверка, если сервер работает больше минуты, выполнить сброс объекта «network», что также прирвёт все соединения с клиентами, и закрыть сервер.
//Мониторим сеть.
event=net.request();
//Здесь можно было бы поместить проверку входящих соединений, но мне лень, поэтому обойдёмся без неё, и так сойдёт :D
//Шучу, и одновременно нет, дело в том, что такая проверка, тут, на самом деле, не требуется, как бы странно это не звучало.
//Для принятия входящих соединений совсем не обязательно делать проверку на наличие событий типа event_connect, так как такое событие лишь указывает на факт соединения, то есть соединение уже установлено, и мы получили сообщение об этом, но реагировать на него совсем не обязательно, можно было бы вообще не делать мониторинг сети, и сделать цикл, который просто закроет сервер через 60 секунд, но нам ещё требуется отреагировать на входящие пакеты.
//Конечно проверку входящих соединений прийдётся делать в более крупных проектах, которые должны загрузить информацию о персонаже, уведомить других игроков о том, что такой-то игрок вошёл в игру, выполнить запись в лог и т.д, но сейчас это совсем не нужно.
//Делаем проверку на входящие пакеты, и шлём их обратно.
if(event.type==event_receive)
net.send_unreliable(event.peer_id, event.message, 0);
//Отправляет присланный пакет приславшему пиру по каналу 0.
/*

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

Каждый участник сетевого соединения, пир, получает уникальный идентификатор, для клиента пиром является сервер, а для сервера все подключённые клиенты являются пирами, и если для клиента идентификатор пира, то есть сервера, имеет не сильно большое значение, то для сервера это единственная возможность различать отправляющих данные клиентов.
Метод connect, который используют клиенты для подключения к серверу, моментально возвращает идентификатор, и в этом нет ничего странного, так как на самом деле, сам клиент генерирует уникальные идентификаторы, то есть какой идентификатор будет использован, решает клиент, в случае подключения к серверу, и сервер тоже сам решает, какой id присвоить для подключаемых к нему пирам.
Идентификаторы начинаются с единицы, и нарастают с новыми подключениями, но даже если какой-то пир прирвёт соединение, освобождённый им идентификатор никогда не будет использован, он будет использован повторно только в том случае, если выполнить метод destroy, а затем setup_client или setup_server, чтобы повторно инициализировать объект network, что подразумевает под собой разрыв соединения со всеми пирами.
Другими словами, все идентификаторы уникальны, и нет ни какой вероятности, что какие-то из них будут использованы повторно или одновременно, однако уникальны они только относительно объекта network, то есть при первом подключении клиента к серверу, клиент присвоит для пира сервера id 1, тогда как сервер тоже присвоит подключившемуся клиенту id 1.
Но если к серверу подключется ещё кто-то, то сервер присвоит новому пиру id 2, когда его клиент присвоит пиру сервера id 1.
Если первый клиент выйдет и зайдёт, и при этом не произойдёт вызов метода «destroy» объекта «network», то при повторном подключение клиент присвоит пиру сервера ID 2, тогда как сервер присвоит вновь подключившемуся клиенту ID 3, так как id 2 уже был использован при подключение второго клиента.
Если вам кажется это сложным для понимания, вы можете пока не обращать внимание на такие тонкости, вам достаточно помнить, что для каждого объекта «network», все присваиваемые идентификаторы всегда уникальны.
Также имеется способ одновременной отправки пакета всем пирам, для этого можно использовать идентификатор 0.
Можно кстати вообще не заморачиваться с запоминанием клиентом идентификатора сервера, а отправлять все пакеты используя 0 в качестве идентификатора, но мне мой подход почему-то кажется более правильным, хоть на самом деле приходится писать немного больше кода.
Но будьте осторожны при использование такого способа отправки, если в качестве текста используется функция, которая может вернуть разные значения.
я даже не знаю, стоит ли считать это багом BGT или намеренно-реализованной возможностью, но так как в справке об этом не упоминается, скорее всего это всё-таки баг.
Дело в том, что при отправке каждому пиру, выполняется повторная отправка сообщения, которое было направлено в функцию «send_reliable или send_unreliable», это не заметно, если используется обычная строка, но если, например, в генерации строки для отправки участвует функция «random», то каждый пир получит разные значения.
Если вы хотите избежать данной проблемы, прежде чем рассылать пакет всем пирам, соберите его в обычную строку, и передавайте её методу «send_reliable или send_unreliable».
И так, теперь вернёмся к нашему серверу.
*/
//В принципе, всё, закрываем цикл и функцию «main».
}
}
//Ну вот, сервер тоже готов, поместите этот код в файл, назовём его "server.bgt"

Заключение

Теперь вы можете запустить файл «server.bgt», если вы всё сделали правильно, то должно появиться сообщение об успешном запуске. Закройте его.

Теперь запустите «client.bgt», в качестве адреса оставляем «localhost», порт 2223.
Должно появиться сообщение об удачном подключение, тоже закрываем его, желательно быстро.
Теперь вы можете нажимать «enter», отправляя тем самым на сервер пакет с текстом «hello», и сервер должен вернуть обратно это приветствие. То есть откроется окно, в котором будет информация о полученном пакете. Не забудьте закрыть его.
Таким образом вы можете обмениваться пакетами с сервером, и сервер будет возвращать вам их обратно, через минуту после запуска сервера, вы должны увидеть сообщение о том, что сервер разорвал соединение, так как сработало условие, которое остановило сервер.

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

Но вот вопрос, нужно ли это кому-нибудь, и стоит ли мне тратить время на это, напишите своё мнение в комментариях, все ваши вопросы можете также задавать там, и удачного вам кодинга!

BGT Network: 0 комментариев

  1. большое спасибо! да как мне кажется много кого интересует эта тема, у каждого наверно есть желание сделать хоть какую-нибудь маленькую, но свою сетевую игрушку, и по играть со своими друзьями!

  2. Благодарю Данил статья действительно получилась хорошей.
    Я сделал как написано в статье и всё получилось.
    Да и хорошо было бы действительно написать статью, о которой ты выше говорил.

Добавить комментарий