Программирование звуковых игр при помощи BGT: Разработка многопользовательской игры

Дата публикации:29.08.2015
Twitter Facebook Vkontakte

Содержание

Класс network

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

Методы

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

Свойства (все свойства доступны только для чтения):

  • int connected_peers -- Содержит число подключившихся игроков или -1 в случае ошибки.
  • double bytes_sent -- Содержит число отправленных байт, включая ошибочные и служебные пакеты, в течение текущей сессии или -1 в случае ошибки.
  • double bytes_received -- Содержит число полученных байт ото всех игроков в течение текущей сессии, включая служебную информацию, или -1 в случае ошибки.
  • bool active -- Содержит значение true, пока объект активен (т.е. между вызовом одного из методов setup_*и вызовом метода destroy). В иных случаях значение будет false.

Класс network_event

Кроме класса network, в bgt существует класс network_event, который содержит информацию о полученных событиях. Объект network_event возвращается методом request() исодержит следующие свойства, доступные только для чтения:

  • int type
  • Тип полученного события. Может принимать любое из следующих значений:
    • EVENT_NONE (0) - Нет событий в данный момент. Это значение по умолчанию.
    • EVENT_CONNECT (1) - Подключился очередной игрок.
    • EVENT_RECEIVE (2) - Получен пакет от одного из игроков.
    • EVENT_DISCONNECT (3) - Отключился один из игроков.
  • uint peer_id
    Уникальный идентификатор участника многопользовательской игры. Это свойство актуально для любых событий, исключая EVENT_NONE.
  • int channel
    Номер канала, к которому относится это событие. Это свойство актуально только для событий с типом EVENT_RECEIVE, и содержит значение 0 в иных случаях.
  • string message
    Данные, полученные по сети. Это свойство актуально только для события с типом EVENT_RECEIVE, и содержит пустую строку в иных случаях.

Уникальный идентификатор игрока

Каждый объект network_event содержит так называемый peer_ID, который представляет собой уникальный идентификатор игрока. Когда клиент пытается подключиться к серверу, метод connect() возвращает ID игрока. Аналогичным образом, когда сервер получает событие network_event, связанное с новым входящим соединением, то это событие будет содержать ID подключившегося игрока, так чтобы сервер мог однозначно идентифицировать этого клиента при отправке и получении данных.

Пакеты

После того как соединение было успешно установлено между двумя или более игроками, эти игроки смогут обмениваться данными, посылая так называемые пакеты. Пакет-это последовательность байт представленная в скрипте BGT в виде строковой переменной. Пакеты BGT не стоит путать с пакетами TCP/IP или иными случаями употребления термина "пакет".

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

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

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

Сложности могут возникнуть при попытке отправить надёжный и ненадёжный пакеты друг за другом. Поскольку пакеты обязаны быть доставлены в том же порядке, что и при отправлении, а надёжные пакеты требуют подтверждения о доставке, они будут задерживать любые ненадёжные пакеты, которые приходят после них. Затем, после того как надёжный пакет будет принят, все задержанные ненадёжные пакеты прибудут сразу. Чтобы избежать этого, bgt использует каналы.

Каналы

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

Следует проявлять осторожность при одновременном использовании нескольких каналов, чтобы избежать ситуации, когда один канал отстает от других во времени, например, из-за пробуксовки нескольких надежных пакетов. Разные каналы полезны, когда один пакет не строго зависит от

другого пакета, доставленного раньше или позже.

Каналы нумеруются, начиная с нуля, что означает, что если было установлено соединение с 3 каналами, то можно использовать каналы 0, 1 и 2 при отправке данных.

Установка соединения

При организации многопользовательской игры средствами BGT необходимо, чтобы один из игроков выступал в качестве сервера, а остальные подключались к этому серверу (то есть выступали в качестве клиентов). Сервер должен иметь возможность прослушивать порт и принимать соединения по протоколу UDP. Клиенты должны знать IP-адрес сервера и порт для подключения к нему.

И в серверном, и в клиентском скрипте BGT объект класса network создаётся конструктором без параметров:

network host;

Затем вызывается один из методов setup_*, чтобы инициализировать его либо в качестве сервера, либо в качестве клиента.

Для инициализации в качестве сервера используется метод

bool setup_server(int port, int channels, int max_peers)

В качестве параметров выступают номер порта (port), количество используемых каналов (channels), максимальное количество активных соединений (max_peers). Метод возвращает true в случае успеха и false в противном случае.

Если вызов этого метода прошёл без ошибок, то сервер сразу перйдёт в состояние ожидания входящих подключений. Далее сервер должен в цикле вызывать метод request() и обрабатывать события network_event с типом, которые содержат информацию о подключившемся игроке.

Теперь вернёмся к коду клиентского скрипта. Для инициализации в качестве клиента используется метод

bool setup_client(int channels, int max_peers)

Параметры: число используемых каналов (channels), но не более 100; предельное число игроков, с которыми может одновременно соединяться этот клиент (max_peers), но не более 4000. В случае успешной инициализации метод возвращает true, иначе -- false.

Если инициализация прошла без ошибок, то для соединения с сервером необходимо вызвать метод

uint connect(string host, int port)

В качестве аргументов метод принимает доменное имя сервера или его IP-адрес (host) и номер порта (port). В случае успешного соединения метод вернёт ID игрока. В случае неудачи возвращается 0.

Затем в цикле необходимо регулярно вызывать метод request(), чтобы получить событие network_event с типом EVENT_CONNECT в случае успешного соединения с сервером.

Обмен данными по сети

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

Первый метод

bool send_reliable(uint peer_id, string packet, int channel)

отправляет "надёжный" пакет. Его параметры: id игрока (peer_id), которому предназначен пакет; отправляемые данные (packet) и номер канала (channel), по которому будет отправлен пакет. Если необходимо разослать пакет всем игрокам, то в качестве id игрока нужно указать 0.

В случае успешной отправки метод возвращает true, иначе -- false.

Второй метод

bool send_unreliable(uint peer_id, string packet, int channel)

отправляет "ненадёжный" пакет. Его параметры такие же, как и у метода send_reliable(). В случае успешной отправки метод возвращает true, иначе -- false.

Для того чтобы узнать актуальную информацию о состоянии сетевого соединения, а также получить поступившие по сети данные, необходимо регулярно вызывать метод request(). Этот метод возвращает объект network_event.

Разрыв соединения

Разорвать соединение с определённым игроком в BGT можно одним из трёх методов: disconnect_peer_forcefully(), disconnect_peer() и disconnect_peer_softly(). Выбор метода связан с тем, как и когда вы хотите, чтобы удаленные участники были уведомлены о вашем отключении. После вызова disconnect_peer() или disconnect_peer_softly() необходимо следить за событиями на хосте, потому что отключения не будет полным до тех пор, пока не будет получено событие network_event с типом EVENT_DISCONNECT и ID отключённого игрока.

Примеры:. сервер и клиент

В качестве примеров реализации сервера и клиента ниже приведён код из документации BGT, дополненный комментариями на русском языке. Этот код не везде эффективен и оптимален, но главное его достоинство -- наглядность. Сервер и клиент должны быть оформлены как отдельные файлы с расширением .bgt. Для целей тестирования оба скрипта можно запускать на одном компьютере и при указании имени сервера использовать:

localhost

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

Сервер

Сервер прослушивает порт 10000 и отсылает обратно все полученные сообщения.

network host;

void main()
{
	show_game_window("Игровой сервер");
	tts_voice voice;
	if(host.setup_server(10000, 1, 100)==false)
	{
		voice.speak_wait("Ошибка. Не удалось инициализировать  сервер.");
		exit();
	}
	voice.speak("Это эхо-сервер. Этот сервер поддерживает 100 одновременных подключений, и отправляет обратно всё, что получает.");
	network_event event;
	while(true)
	{
		event=host.request();
		if(event.type==event_connect)
		{
			voice.speak_interrupt("Клиент с номером " + event.peer_id + "  подключился с адреса " + host.get_peer_address(event.peer_id) + ".");
			voice.speak("Всего клиентов: " + host.connected_peers + ".");
		}
		if(event.type==event_receive)
		{
			host.send_unreliable(event.peer_id, event.message, event.channel);
		}
		if(event.type==event_disconnect)
		{
			voice.speak_interrupt("Клиент с номером " + event.peer_id + " разорвал соединение.");
			voice.speak("Всего клиентов: " + host.connected_peers + ".");
		}
		if(key_down(KEY_LMENU) and key_pressed(KEY_F4))
		{
			voice.speak_interrupt("Прекращаю работу.");
			disconnect_everyone();
			// Ожидаем, пока синтезатор завершит фразу.
			while(voice.speaking==true)
			{
				wait(5);
			}
			exit();
		}
		wait(5);
	}
}

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

Обратите внимание, что в цикле обработки сетевых событий отсутствует обработка полученных пакетов. 
Однако в реальной игре это придётся делать, так как по другим соединениям будут приходить данные.
*/
void disconnect_everyone()
{
	timer timeout;
	uint[] peer_list=host.get_peer_list();
	int expected_disconnects=peer_list.length();
	for(uint i=0;i < peer_list.length();i++)
	{
		host.disconnect_peer(peer_list[i]);
	}
	network_event event;
	while(expected_disconnects>0)
	{
		event=host.request();
		if(event.type==event_disconnect)
		{
			expected_disconnects-=1;
		}
		if(timeout.elapsed>=1000)
		{
			break;
		}
		wait(5);
	}
}

Клиент

Клиент соединяется с сервером и собирает статистику соединения.

network host;

void main()
{
	show_game_window("Игровой клиент");
	tts_voice voice;

	// Используется, чтобы сохранить ID удалённого игрока,
	// который выступает в качестве сервера.
	uint server_id=0;
	if(host.setup_client(1, 1)==false)
	{
		voice.speak_wait("Ошибка. Не удалось инициализировать клиента.");
		exit();
	}
	voice.speak("Клиент работает.");
	voice.speak("Введите ip-адрес сервера или его доменное имя.");
	string address=input_box("Адрес", "Введите адрес или имя игрового сервера.");
	if(address=="")
	{
		voice.speak_interrupt_wait("Прекращаю работу.");
		exit();
	}
	voice.speak_interrupt("Выполняется подключение к серверу...");
	host.connect(address, 10000);
	network_event event;
	// В этом цикле ожидается событие,
	// которое всегда возникает при успешной или неуспешной попытке 
	// установить связь с сервером.
	while(true)
	{
		event=host.request();
		if(event.type==event_connect)
		{
			voice.speak_interrupt("Подключились к серверу " + host.get_peer_address(event.peer_id) + "!");
			server_id=event.peer_id;
			break;
		}
		if(event.type==event_disconnect)
		{
			voice.speak_interrupt_wait("Не уудалось подключиться к серверу " + 
				host.get_peer_address(event.peer_id) + " Прекращаю работу.");
			exit();
		}
		if(key_down(KEY_LMENU) and key_pressed(KEY_F4))
		{
			host.destroy();
			voice.speak_interrupt_wait("Прекращаю работу.");
			exit();
		}
		wait(5);
	}

/*
	Соединение установлено и можно начать сбор статистики.
	Будут посланы 30 пакетов каждую секунду в течение 10 секунд.
	Затем соединение будет разорвано и будет озвучена статистика.
*/

	timer message_trigger;
	timer total_time;
	int sent_messages=0;
	int received_messages=0;
	while(true)
	{
		event=host.request();
		if(event.type==event_disconnect)
		{
			host.destroy();
			voice.speak_interrupt_wait("Сервер разорвал соединение. Прекращаю работу.");
			exit();
		}
		if(event.type==event_receive)
		{
			received_messages+=1;
		}
		if(message_trigger.elapsed>=33)
		{
			message_trigger.restart();
			host.send_unreliable(server_id, "Привет, сервер!", 0);
			sent_messages+=1;
		}
		if(total_time.elapsed>=10000)
		{
		// Статистика собрана. Можно её озвучить
			voice.speak_interrupt("Статистика: Отправлено сообщений: " + sent_messages + ". Получено сообщений: " + received_messages + ". Среднее время прохождения пакета: " + host.get_peer_average_round_trip_time(server_id) + " миллисекунд. Прекращаю работу.");
			disconnect_everyone();

/*
	Этот цикл нужен, чтобы сообщение о статистике прозвучало полностью.
	Но пользователь, если не желает ждать, 
	может завершить работу приложения,
	нажав Alt+F4.
*/
	while(voice.speaking==true)
	{
		if(key_down(KEY_LMENU) and key_pressed(KEY_F4))
		{
			break;
		}
		wait(5);
	}
	exit();
		}
		if(key_down(KEY_LMENU) and key_pressed(KEY_F4))
		{
			voice.speak_interrupt("Прекращаю работу.");
			disconnect_everyone();
			// Ожидаем, пока синтезатор договорит фразу.
			while(voice.speaking==true)
			{
				wait(5);
			}
			exit();
		}
		wait(5);
	}
}

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

Обратите внимание, что в цикле обработки сетевых событий отсутствует обработка полученных пакетов. 
Однако в реальной игре это придётся делать, так как по другим соединениям будут приходить данные.
*/
void disconnect_everyone()
{
	timer timeout;
	uint[] peer_list=host.get_peer_list();
	int expected_disconnects=peer_list.length();
	for(uint i=0;i < peer_list.length();i++)
	{
		host.disconnect_peer(peer_list[i]);
	}
	network_event event;
	while(expected_disconnects>0)
	{
		event=host.request();
		if(event.type==event_disconnect)
		{
			expected_disconnects-=1;
		}
		if(timeout.elapsed>=1000) break;
		wait(5);
	}
}


Распространение материалов сайта означает, что распространитель принял условия лицензионного соглашения.
Идея и реализация: © Владимир Довыденков и Анатолий Камынин,  2004-2017