Приветствую, дамы и господа, я вам покушать принес.
Итак, Game Maker networking. Штука эта существует уже приличное время, но, тем не менее, русскоязычных уроков по ней в интернетах по непонятной мне причине не найти.
Сегодня я расскажу вам, как создать простейший сервер и клиент, используя только TCP соединение(в рамках текущей статьи UDP рассматривать не буду, поскольку еще не разбирался в нем. Возможно про него напишу вторую часть).
Начну с небольшой теории и терминологии.
В статье, под сервером я буду иметь в виду главную "управляющую" программу(не какой то мифический системный блок, нет нет!). Главная особенность сервера, и почему он нужен - тот кто его разработал, имеет над ним полный контроль. Никакой пользователь не может получить доступ к серверу, и менять по своей прихоти какие либо данные внутри него, в отличии от клиента(за исключением дыр которые допустил сам разработчик, конечно).
Клиент - программа - "визуализатор". В идеале, клиент лишь отображает информацию, пришедшую с сервера. Опять же, в идеале, клиент не должен делать никаких рассчетов(передвижение игроков, запрет прохода в недоступные зоны, расчет попаданий в какие-либо объекты или других игроков). Через клиент пользователь как бы общается с сервером, говорит ему что он хочет(движение персонажа, стрельба, и пр.), сервер в свою очеред обрабатывает желание пользователя и возвращает клиенту ответ - можно двигаться\стрелять\вы попали в цель. Клиент же выводить все это на экран по встроенным в него алгоритмам.
Собственно, в этом и заключаются сложности при разработке многопользовательских игр, и это те причины, почему переводить однопользовательский проект в мультиплеер пожалуй даже сложнее и дороже, чем писать аналогичный, но заточенный под мультиплеер - с нуля.
2. Подготовка и немного теории
С прелюдией закончил, перейдем к делу. Для начала приведу список того что нам нужно для разработки. В конце статьи приложу исходники сервера\клиента и собранные сервер\клиент, которые можно протестировать у себя. Напомню, что использовать будем исключительно TCP протокол, не будем заниматься никакими оптимизациями и создавать сложных механик.
Итак. Основные принципы работы вкратце. Первым этапом нашего бизнес-процесса является запуск сервера. Что делает сервер? Он открывает порт для прослушки. Все. Пока к нему не подключится клиент, ему больше ничего делать не нужно. Делается это командой network_create_server().
Но конечно же, на самом деле это лишь видимая часть. Помимо этого, разумно сразу при запуске подготовить к работе некоторые другие части программы. О них мы будем говорить уже при подробном разборе полетов, а пока позанимаемся абстракцией.
Далее в игру вступает клиент. Опустим все вещи которые он должен делать при запуске(они очень индивидуальны для каждого проекта и к сети отношения не имеют - инициализация и подгрузка ресурсов, данных и пр.). После всего этого, нам нужно где то получить ip и port нашего сервера(далее буду писать все по русски).
В нашем случае, мы зададим порт и адрес, просто вбив его вручную. Порт мы вошьем прям в код, а адрес спросим у пользователя.
Для того, чтобы создать соединение, нам нужен сокет(socket). Его создает функция network_create_socket. В качестве параметра ей указываем тип соединения, которое наш сокет будет использовать. Далее, клиент осуществляет коннект с сервером с помощью функции network_connect(), один из параметров которой является сокет, созданный в предыдущем шаге, и теперь через него мы можем осуществлять передачу пакетов.
Во время исполнения коннекта на клиенте, на сервере студией генерируется структура, ds_map. Эта структура по сути является списком записей типа(<ключ> <значение>), где ключ - просто строковое значение, а значение - соответственно что угодно другое. Именно через эту самую ds_map с названием async_load и происходит обмен данные по TCP протоколу между сервером и клиентом. Именно в этом и есть основное отличие работы в networking в сравнении с другими популярными онлайн-библиотеками(типа 39dll).
Далее, мы начинаем обмен пакетами между сервером и клиентом. Осуществляется это с помощью записи данных в буфер, и последующей отправкой буфера с помощью network_send_packet(). Но обо всем по порядку.
3. Этап 1: Connection
Начнем реализацию проекта. В нашем проекте, в роли игроков будут выступать треугольники, которые будут следить и поворачиваться вместе с мышью. игроки будут динамически появляться и исчезать друг у друга при открытии\закрытии клиентов. Я не претендую на абсолютную правильность и оптимизированность реализации, более того, некоторые не относящиеся напрямую к сети моменты я специально реализовал неоптимальными методами, позволяющими не отвлекаться от сути.
Итак, работаем в 2 окна. Создаем в них по проекту, и называем - 1 сервер, второй - клиент. Начнем с разработки механики клиента.
для начала спрайт игрока. Рисуем какой нибудь спрайтик, или берем готовый, или берем мой красный треугольник. Не забываем задать точку вращения! Для моего спрайта она будет x = 9, y = 15. Создаем объект o_player и obj_othplayers. Все доп флаги(visible, solid) оставляем по умолчанию(для всех будущих объектов тоже не трогаем флаги, они нам незачем). Задаем им наш спрайт. Открываем o_player и создаем в create ему кусок кода, инициируем некоторые переменные:
Код
x_prev = x; y_prev = y;
Эти значения будут нам нужны только для игровой механики, и сеть пока мы никак не затрагиваем. Затем создаем ему же ивент Global left button(не pressed и не released), и записываем
Код
if point_distance(x, y, mouse_x, mouse_y) > 2 { x = mouse_x; y = mouse_y; direction = point_direction(x_prev, y_prev, x, y); image_angle = direction; x_prev = x; y_prev = y; }
Этот до жути простой код отвечает за движения нашего игрока-треугольника.
Наш игрок готов. Создаем комнату, создаем там игрока. Запускаем, убеждаемся что наш объект следует за мышкой.
Теперь там же, в клиенте, создаем объект(назову его o_netw), который уже будет отвечать за работу с сетью. В create ему запишем следующее:
Код
client = network_create_socket(network_socket_tcp); ip = get_string("Enter server ip", "127.0.0.1"); network_connect(client, ip, 10001);
Вот наши 3 строчки, которые будут соединять сервер и клиент. К сожалению, тут есть небольшая недоработка(хотя может конечно я что то не понял) - мы не можем проверить успешно ли мы законнектились с сервером. Для того чтобы это узнать, нам нужно посылать контрольный пакет, и если мы получили ответ от сервера - тогда все прошло успешно, если таймаут истек - значит ответа нет. В рамках статьи мы заниматься этим не будем и будем считать, что с сервером у нас все хорошо
Далее, мы туда же дописываем инициализацию буфера, через который будем отсылать данные на сервер:
Код
buffer_send = buffer_create(64, buffer_grow, 1);
Более подробное инфо про эту функцию вы можете прочитать прямо в справке, там много информации, потому не вижу смысла ее всю приводить. Скажу лишь, что параметры нашей функции означают, что мы создаем динамический буфер с максимальным размером в 64 знака, чего более чем достаточно для нашего проекта.
Так же зададим переменную для того, чтобы не отсылать данные каждый такт:
Код
delay = 0;
Ну и последний пункт - инициируем наш массив, в котором будем хранить ссылки на объекты других игроков:
Код
for(i=0;i < 10; i++) { players[i] = -1; }
Наш массив будет содержать 10 элементов. Конечно правильней использовать ds_list для таких вещей, но мы обойдемся максимально простыми записями . Опять же, углубляться в лишние проверки для реального ограниченя не будем, так что можете поставить здесь число побольше.
Напомню, что весь этот код должен содержаться в Create объекта o_netw. Не забываем добавить его в игровую комнату.
Итак, первые шаги сделаны. Перейдем к серверу. После этого шага мы уже сможем увидеть наших запущенных клиентов на сервере(но не друг у друга!).
Переходим в окно сервера. Создаем там такой же спрайт игрока как и в клиенте(хотя можно и другой, конечно). Не забываем про точку вращения .
Итак, в первую очередь создаем объект o_server. Идем к нему в create и пишем следующий код:
Ну тут в принципе, опять же, пояснять нечего. Задаем порт, ставим его на прослушку, инициализируем буфер для сообщений, обнуляем массив для игроков.
Остановимся на минутку. Теперь мы имеем сервер, который ждет клиента, и клиент, который посылает коннект. Базовая связь, ничего лишнего. Чтобы проверить как все работает, нам остался лишь 1 шаг - нужно создать event в проекте-сервере, в объекте o_server. Открываем наш объект, жмем create event - Asyncronous - Networking. Именно через него происходят все обмены данными в GMS networking. Собственно теперь и начинается самое интересное.
итак, создаем код в networking:
Код
eventid = ds_map_find_value(async_load, "id"); type = ds_map_find_value(async_load, "type"); sock = ds_map_find_value(async_load, "socket"); ip = ds_map_find_value(async_load, "ip");
Объясню что здесь происходит. Как только сервер улавливает сетевую активность(проще говоря, приходит сообщение), студия генерирует ds_map с названием async_load(то, о чем я писал в начале) и срабатывает ивент networking. Обращаясь к этой структуре, мы сразу забираем основные поля, нужные нам для дальнейшей работы, и созраняем их значения в переменные. Все пакеты мы теперь будем описывать здесь. Собственно, начнем с того что напишем обработку подключения клиента(то что нам сейчас и нужно):
Как видите, все просто. Мы просто создаем в середине экрана объект игрока если type = network_type_connect. network_type_connect - это встроенная константа, она означает что тип пришедшего сообщения - подключение. Помимо этого из нужных нам есть network_type_data, которая обозначает передачу данных - то есть собственно вся наша работа, и network_type_disconnect, означающая отключение клиента.
осталось только создать объект игрока o_player(напомню, в сервере), назначить ему спрайт, создать комнату с o_server внутри - и можно тестировать! Если по каким то причинам приложения не работают - попробуйте поменять 10001 в клиенте и сервере на какое нибудь другое число. Это номер порта, по которому происходит связь наших приложений. Про доступные порты можете поискать в гугле, но в принципе можно использовать практически любой число до 65536(если мне не изменяет память ).
Порядок теста - запускаем сервер, запускаем клиент. После запуска клиента в окне сервера должен появиться наш игрок. Если появился - значит все прошло как нало, если не появился - сверяйтесь с уроком, если зависло окно клиента - значит проблема с соединением. Меняйте номер порта, или опять же сверяйтесь с уроком. Имейте ввиду, что для гарантии работы IP должен быть тот что стоит по умолчанию - 127.0.0.1.
Окей, предположим что все хорошо и работает. Теперь научим наш объект двигаться!
4. Этап 2: Передача координат
Для этого нам просто нужно передать координаты на сервер, а на самом сервере - присвоить принятые координаты к объекту игрока. Обо всем по порядку. Возвращаемся к нашему окну клиента. Открываем объект o_netw, создаем ему step event. Пишем:
Разбираем написанное. Переменная delay отвечает за то, чтобы мы не перегружали сеть лишними данными. Благодаря ей, данные будут передаваться раз в 3 такта игры. Итак, если мы передаем данные(delay > 2) тогда обнуляем переменную. С помощью buffer_seek мы чистим буфер, для того чтобы там не было ненужного мусора. Это очень важно! Если мы не очистим буфер, тогда предыдущее и текущее сообщение перепутаются, и последствия могут быть самыми неожиданными. Поэтому перед формированием любого сообщения, в первую очередь выполняем buffer_seek. Константа buffer_seek_start в качестве параметра означает, что мы сбрасываем указатель записи на старт, а все "ячейки" буфера примут значение "0"(последний параметр). Функция - аналог функции 39dll_buffer_clear() из 39.dll.
А теперь мы просто поочередно записываем нужные нам данные в этот буфер через функцию buffer_write. Каждая запись создается в конце идущей перед ней, поэтому мы просто несколько раз вызываем функцию для последовательной записи наших данных. Второй параметр(buffer_s16) означает тип записываемых данных. Очень важно следить за типами. Подробнее о типах можете прочитать в справке и гугле, в данном уроке мы обойдемся лишь одним. Ну и наконец отсылаем наш буффер через функцию network_send_packet(). Последний параметр - это размер отсылаемого сообщения. В большинстве случаев эту конструкцию менять не нужно.
Теперь идем в проект сервера. Нам нужно описать прием данных. Идем в event Networking в obj_server и немного модернизируем наш код в нем. А именно, дописываем в конец:
В принципе, ничего сложного в этом коде нет. Единственный момент, который нужно здесь запомнить - eventid - это идентификатор отправителя. Этот идентификатор приходит в переменную sock при коннекте клиента(смотри часть когда type = network_type_connect). В остальном - все легко и просто. Можно тестировать
Теперь мы легко можем увидеть нескольких игроков. Для этого нужно запустить сервер, а проект клиент сохранить как исполняемый файл и просто запустить несколько раз Теперь треугольники двигаются на сервере точно так же как и в клиентах, вот только остался один нюанс - игроки все еще не видят друг друга. Кстати, уже сейчас вы можете собрать клиент на любые другие платформы и управлять треугольничком на сервере например с телефона :)
5. Этап 3: Знакомим клиентов друг с другом
Теперь, нужно научить игроков видеть друг друга. Начнем с того, что слегка модифицируем серверную часть. Во первых, мы не будем считать игроков активными, пока они не прислали координаты.Для этого открываем проект сервера, лезем в o_player и пишем ему в create:
Код
initiated = false
Это наша заглушка, означающая что игрок только только подключился и пока не проявил активности. Теперь залезем в o_server и добавим в конец create еще 1 строку:
Код
global.n_max = 0;
Эта переменная будет содержать в себе максимальный индекс подключенного клиента в массиве игроков. Говоря проще, нам незачем проверять все 10 элементов массива, если у нас подключено только 2 игрока, и эта переменная нам поможет.
Теперь нам понадобятся 2 скрипта, которые мы должны создать на сервере. Итак, создадим их.
Первый скрипт будет называться send_to_all_except_one_tcp. Создаем его и пишем в нем код:
Код
i = 0; while (i<= global.n_max) { if i != argument0 { if o_server.players[i] != -1 { if o_server.players[i].initiated = true { network_send_packet( o_server.players[i].sock, buffer_send, buffer_tell(buffer_send) ); } } } i++; }
Этот скрипт позволяет нам отослать сообщение, предварительно записанное в buffer_send всем игрокам, кроме того что указан в аргументе. Ничего сложного, если вдуматься.
Второй скрипт будет называться send_starting_info. Вот его код:
Код
i = 0; while (i<= global.n_max) { if o_server.players[i] != -1 && i != eventid { buffer_seek(buffer_send, buffer_seek_start, 0); buffer_write(buffer_send, buffer_s16, i ); buffer_write(buffer_send, buffer_s16, 0 ); network_send_packet( eventid, buffer_send, buffer_tell(buffer_send) ); } i++; }
Этот скрипт формирует и отсылает нашему новому игроку данные обо всех игроках, которые уже на сервере.
Теперь нам нужно слегка модифицировать o_server в проекте сервера.
Разберем что поменялось. Во первых, в начале добавился блок, который обеспечивает нового клиента данными обо всех остальных игроках. заглушка initiated обеспечивает, что игрок эти данные получит лишь раз. На что стоит обратить внимание - это строки:
Это что то вроде заголовочной части пакета. Первая строка содержит в себе уникальный идентификатор нашего изначального отправителя координат. Этот идентификатор един во всей нашей системе - под ним он находится в массивах у клиентов и сервера. Вторая строка определяет тип сообщения. например я для себя решил, что 1 - это идентификатор сообщения, несущего координаты. Если расширять проект, то с цифрой 2 можно отправлять например стрельбу, и соответственно на клиентах по другому расшифровывать наш пакет. В нашем примере с кодовым номером 0 идет команда клиенту на создание другого игрока, а под номером 2 - на уничтожение. Вот это все - самый сложный для понимания момент. Ну а тем временем мы идем дальше.
туда же, добавялем в конец строки:
Код
if type = network_type_disconnect { with (players[sock]) { instance_destroy(); } o_server.players[sock] = -1; buffer_seek(buffer_send, buffer_seek_start, 0); buffer_write(buffer_send, buffer_s16, sock ); buffer_write(buffer_send, buffer_s16, 2 ); send_to_all_except_one_tcp(sock); }
Это обработка отключения игрока. Если к нам пришло сообщение с типом disconnect(оно автоматически генерируется например при закрытии окна с клиентом) - удаляем его объект с сервера, освобождаем его место в массиве игроков, и рассылаем всем игрокам сообщение о том, что наш клиент уехал к родственникам в Совнгард. Обратите внимание на то, что здесь в качестве идентификатора используется снова sock а не eventid.
Теперь нам нужно только добавить возможность клиентам понимать приходящие пакеты. Открываем проект-клиент, лезем в объект o_netw и создаем ему event Networking, как мы уже делали раньше для o_server.
Создаем в нем кусок кода, и пишем:
Код
t = ds_map_find_value(async_load, "type");
if t = network_type_data { msg_buff = ds_map_find_value(async_load, "buffer"); sender = buffer_read(msg_buff, buffer_s16); type = buffer_read(msg_buff, buffer_s16); switch(type) { case 0: players[sender] = instance_create(0,0,obj_othplayers); break;
case 2: with(players[sender]) { instance_destroy(); } players[sender] = -1; break;
} }
Как видите, тут выходит все немного проще чем на сервере. Здесь нам нужен лишь тип сообщения в начале кода, а дальше мы его обрабатываем с помощью конструкции switch:case. Это и есть та самая обработка заголовков, о которой я писал немного выше. Тут мы разбиваем сообщения на типы, и из каждого сообщения получаем те данные, которые от него ожидаем, основываясь на его заголовке.
Вот собственно и все. теперь можете запустить свой проект и убедиться, что игроки стали видеть друг друга, что они могут появляться и исчезать, и все это ужасно дергано :)
XDominator, Спасибо большое ! А разве в клиенте можно делать движение ? Можно и взломать ! Но пока это не страшно ) Создать пулю оказалось создать очень сложно, я этого не смог сделать, а вот продолжить работу с этим же объектом и изменить его цвет просто Всё что я сказал может показаться обидно , но это только кажется так, ибо мнение моё и оно может поменяться.
Сообщение отредактировал GMHelp - Вторник, 15 Июля 2014, 12:37
Сделал бы все разноцветное, а то читать тошно все выглядит как сплошной текст, ИМХО, нужно заголовочки, там, выделить, по пунктикам, списочки, там, всякие, из справки какие-нибудь интересные факты, ну а в общем все понравилось, кстати, я был твоим фанатом, но теперь я еще больше твой фанат, без шуток, спасибо))))
Ну да, думаю стоит отформатировать все это добро, или разбить по частям. К сожалению мои дизайнерские способности оставляют желать лучшего, но тем не менее я постараюсь Ghaarp
7. Больше объектов. А на самом деле как работать с динамичными объектами , которые то появляются, то исчезают ? Надо добавлять в connect их или что ? Очень интересно узнать. И upd увидеть тоже хочется. buffer_s16 и другие... раскажешь ? Мог сделать видосы и впихнуть ) как вариант Всё что я сказал может показаться обидно , но это только кажется так, ибо мнение моё и оно может поменяться.
Сообщение отредактировал GMHelp - Вторник, 15 Июля 2014, 15:41
Неохота заморачиваться с видео если честно) да и хуже оно все таки чем просто текстом, текст всегда можно поправить если что то забыл или не так сказал. насчет динамических объектов - с ними работа не отличается от того что я привел выше, с той лишь разницей что нам не нужно передавать их координаты, например. Если мы стреляем на клиенте, то мы на самом деле не стреляем, а посылаем пакет серверу, который как бы говорит - хочу стрелять. Если сервер разрешает(т.е. нет откатов или еще чего либо) - тогда всем игрокам рассылается пакет, что такой то клиент выстрелил. И все клиенты создают в указанной точке объект-пулю. А дальше эта пуля живет по одинаковым законам на клиенте и на сервере, с той лишь разницей что например попадания на клиентах учитываться не будут, а будут только на сервере, и если пуля попала - опять же сервер отсылает всем клиентам инфо что такая то пуля попала туда то и ее нужно уничтожить. Из-за времени отклика тут конечно очевидны сразу проблемы, связанные с пингом и всем таким, но в целом - вполне юзабельная схема Ghaarp
XDominator, как раз об этом и подумал. Ох... буду стараться сделать. Ещё уроки будут ? Всё что я сказал может показаться обидно , но это только кажется так, ибо мнение моё и оно может поменяться.
XDominator, пинг это не твоя проблема, а проблема уже пользователей (нечего скупиться на быстрый интернет), тебе лишь нужно все оптимизировать по-максимуму
XDominator, не смог я делать динамичные объекты. Научиии Всё что я сказал может показаться обидно , но это только кажется так, ибо мнение моё и оно может поменяться.
Боже.. Игрок нажимает ЛКМ -> на сервер приходит команда, что нужно создать пулю -> сервер создает пулю -> каждый шаг двигает ее и передает координаты клиентам
Сообщение отредактировал aFriend - Среда, 16 Июля 2014, 18:18
Все в принципе так как описал aFriend, только вот последний пункт не нужен, пуля двигается в клиентах, а сервер сообщает клиентам только о случаях столкновения пуль с чем-либо:) Ghaarp
нельзя, будь внимательней) Смысл взлома в том чтобы обеспечить попадание, в нашем случае. Ты можешь взломать клиент и послать ее хоть в какую сторону, и попать ей хоть 100 раз в другой объект, но это не будет иметь смысла, потому что столкновения просчитываются на сервере А так да, можно и взломать, только вот пользы от такого взлома... Ghaarp
Так коллизии мы учитываем только на сервере, какая разница что ты взломаешь на клиенте? ну подвинешь пулю к цели, но условием отъема хп цели является пришедший пакет об этом с сервера, клиент не отправляет данных о попаданиях. В этом и суть. Ghaarp
Если она попала на сервере - отнимется. Тогда происходит рассылка всем клиентам о попадании пули в такую то цель. Но попадания на клиентах не должны учитываться в игре, тут ты прав. Ghaarp
Я к тому клоню, что тебе придется больше действий сделать: и проверять столкновение на сервере, двигать на сервере и двигать в клиенте, почему бы просто не делать все на сервере, а клиенту отправлять лишь данные?
Потому что зачем нам отправлять данные о движениях пуль, если клиент их может рассчитывать сам? Пуля в клиенте - всего лишь необходимая визуализация, но никакого практического смысла она не несет, кроме сообщения юзеру того что "она где то тут есть". А какой смысл ломать визуализацию? А вот если таких пуль у нас скажем одновременно сотня - и постоянно отправлять их координаты - вот это действительно серьезная недоработка по оптимизации.
Добавлено (18.07.2014, 13:10) --------------------------------------------- Хороший пример такого "взлома" - скажем взлом адены в lineage 2 с помощью artmoney. Ты можешь нарисовать любое число в инвентаре и ходить с ним, но как только количество адены изменится - то тебе с сервера придет новое, реальное число адены. А так да, ее можно нарисовать сколько угодно)))