Ну да, думаю стоит отформатировать все это добро, или разбить по частям. К сожалению мои дизайнерские способности оставляют желать лучшего, но тем не менее я постараюсь Ghaarp
Приветствую, дамы и господа, я вам покушать принес.
Итак, 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. Это и есть та самая обработка заголовков, о которой я писал немного выше. Тут мы разбиваем сообщения на типы, и из каждого сообщения получаем те данные, которые от него ожидаем, основываясь на его заголовке.
Вот собственно и все. теперь можете запустить свой проект и убедиться, что игроки стали видеть друг друга, что они могут появляться и исчезать, и все это ужасно дергано :)
Overdrave, можешь смело рисовать анимацию через draw_sprite(). от одного вызова этой функции за такт фпс уж точно не просядет, так же как и от десятка, да и от сотни, честно говоря, тоже. Так что юзай :). Ghaarp
Видимо, это какой то твой личный скрипт? Во встроенном языке такой функции точно нет, так что у тебя должен быть скрипт SendKey в папке scripts, в если у тебя его там нет, значит его нужно завести. Ghaarp
Автор, для мультиплеера в GMS есть механизм - GMS networking. Если поищешь по такому тегу в гугле, то наткнешься на пару годных примеров. Правда на англ языке . Ghaarp
Создавать сетевые игры в любом случае задача не для новичка, так что рекомендую об этом забыть пока не сможешь с закрытыми глазами реализовывать базовые механики, такие как движение, интерфейсы, сложные взаимодействия объектов. А уже после этого можно переходить к мультиплееру. Ghaarp
Ну чтобы его отобразить, его сперва нужно загрузить, наверное? Этот ивент как раз участвует в процессе загрузки изображения, если верить названию. Найдешь справку о нем - найдешь и пример загрузки изображения с интернета. А после того как сохранишь это изображение в памяти, сможешь его отрисовывать. логично?
Добавлено (04.07.2014, 15:43) --------------------------------------------- Собственно, как я и говорил. Вот вырезка из справки, из раздела async events:
Image Loaded and Sound Loaded Events
This is the sub event that will be triggered by any resource call-backs received.
These two events are triggered when you load an image or a sound into GameMaker:Studio, as long as you have used a valid URL with the applicable load file function. For example say you want to load a background image, and only change the current background to the new one when it has loaded. Well you would have something like this in a create event or an alarm event (for example):
back = background_add("http://www.angusgames.com/game/background1.png", 0, 0);
This will now start to load the image into the device or the browser, but it will not block GameMaker:studio while it waits for the file to be loaded. Instead GameMaker:Studio will keep running as normal until the image is loaded and the call back triggers the Image Loaded event, where a ds_map (more commonly known as a "dictionary") is created and stored in the special variable async_load. The map contains the following information:
◦"filename": The complete path to the file you requested.
◦"id": The ID of the resource that you have loaded. This will be the same as the variable that you have assigned the resource to.
◦"status": Returns a value of less than 0 for an error.
You would then assign the newly loaded image to a background in this event. The above is also true for sprites and sounds, with a ds_map being generated for each of these resources as shown above, and the following code example demonstrates how the returned information would be used in this event:
if ds_map_find_value(async_load, "id") == back { if ds_map_find_value(async_load, "status") >= 0 { background_index[0] = back } }
The above code will first check the id of the ds_map that has been created, then check the status of the callback. If the value is greater than or equal to 0 (signalling success) the result from the callback will then be used to set the background index to the newly loaded image.
NOTE : The variable async_load is only valid in these events, as the ds_map that is points to is created at the start of the event, then deleted again at the end, with this variable being reset to a value of -1.
Добавлено (04.07.2014, 15:46) --------------------------------------------- Да и еще, касательно отрисовки в game maker studio. Я уже в нескольких местах создавал темы, но ответа мне никто так и не смог дать на это. Дело в том, что в студии был переделан алгоритм обработки внешних изображений, и теперь в GMS нельзя запихать программно картинки никакого формата, кроме PNG. Не знаю, как будут вести себя картинки загруженные с инета, но скорее всего так же. Так что скорее всего, отрисовать gif у тебя не получится, по этой самой причине. Может в 1.3 это исправили, или исправят в будущем, но в 1.2 именно так.
Если у тебя вылетают ошибки, связанные с исчезновением объекта, значит ищи строчку, в которой происходит ошибка, и клади на нее сверху условие - if instance_exists(object). Ошибки вылезают из-за обращения к несуществуюшему объекту, а ты с помощью этой функции однозначно обходишь такую ситуацию. Ghaarp
Конечно не так. global.cubes_count - глобальная переменная. Она доступна из любого места программы, и она является твоим счетчиком оставшихся для разрушения блоков. Ты можешь ее назвать хоть abc, но должен стоять префикс global. как директива, что переменная - глобальная. Именно из нее ты вычитаешь единицы( с помощью оператора -- или -=1) при уничтожении блока, и когда именно она достигает 0, запрещаешь уничтожение блоков. creation code комнаты - код, который выполняется при открытии комнаты, задается во вкладке settings комнаты, кнопка creation code. Там ты инициализируешь глобальную переменную. При нажатии на блок, ты проверяешь значение этой глобальной переменной. Если оно больше нуля(строчка if global.cubes_count > 0) - тогда выполняешь код уничтожения блока. Если же она не проходит проверку - значит 3 блока уже были уничтожены, и поэтому ничего не происходит. Ghaarp