Networking Overview

This tutorial aims to cover the basics of the Networking functions in GameMaker:Studio. This tutorial is a basic overview of how to set up a network game using web sockets, and is intended as a complement to the networking demo included with GameMaker:Studio and the YoYo Games tech blog article, Introduction to GameMaker: Studio Networking.

This tutorial will not tell you how to set up a MMO, and be aware that working with networking on any platform can be very frustrating (Even big companies can subcontract the networking multi-player parts of their games out to other small companies to make) and you will need time and patience to get it working, especially if it is new to you. Don't be to ambitious when starting out and test all the networking parts of your game constantly to avoid any unexpected errors later on.

The Basics

Simple networking can be achieved using the GameMaker:Studio networking functions, which can permit you to make small multiplayer games. However if you have never done any type of multiplayer game, or are unfamiliar with web sockets, then it can be quite tricky to set up correctly. This tutorial is designed to give you an overview of a basic networking setup, showing the minimum necessary steps to take and the appropriate functions and actions needed.

For those of you new to this, it can help to think of networked games using a real-world analogy - that of the standard postal service. If you send a letter, that letter goes along with everyone else's letters to the central post office to be sorted and dispatched to their destination. Well, networking over the internet is the same, with one client sending data "packets" to a server which will then send it on to the other clients (or back to the original).

Server Types

For any networking to be done using GameMaker:Studio, you will need to have a server set up to receive and send on the data packets from the clients. In general there are two kinds of servers available to us:

  • A dedicated server is one that is independent of the people playing the game and all it does it receive and re-send data packets. This is most useful for MMO's, action games, and generally any game where lag and size can become an issue.
  • An all-in-one server is one which is hosted by at least one of the players of the game (making it a client and a server). This is most useful for co-op gaming, small scale strategy games, turn-based games, etc...

The actual programing part for both server types is more or less the same, with the same basic functions being used, however there is one important difference between them - for a dedicated server you will need two projects, one for the client and another other for the server (Where in the server project has little or no gameplay elements and is purely for networking). However for an all-in-one client/server you will need to incorporate the server functions into a single game project, and when you create the server, immediately you must to create a client which should connect itself to the server.

Creating A Server

The socket based networking functions of GameMaker:Studio make it incredibly easy to set up your server (no matter whether it is a dedicated server or an all-in-one), as it is done through the use of just one function:

network_create_server(type, port, max_client);

The type is one of two constants:

 Type Constant  Description

 network_socket_tcp 

TCP stands for Transmission Control Protocol. Using this method, the game sending the data connects directly to the server it is sending the data it to, and stays connected for the duration of the transfer. With this method, the two computers can guarantee that the data has arrived safely and correctly. This method of transferring data tends to be quick and reliable, but puts a higher load on the game as it has to monitor the connection and the data going across it.

 network_socket_udp 

UDP stands for User Datagram Protocol. Using this method, the game will release the data packages into the network with the hopes that it will get to the right place (the server). What this means is that UDP does not connect directly to the server like TCP does, but rather sends the data out and relies on the devices in between the client and the receiving server to get the data where it is supposed to go properly. This method of transmission does not provide any guarantee that the data you send will ever reach its destination! On the other hand, this method of transmission has a very low overhead and is therefore very popular to use for services that are not that important to work on the first try.

The port is what is used to connect your server to the internet for sending and receiving data. Every device on the internet must have a unique number assigned to it called the IP address. This IP address is used to recognize your particular device out of the millions of other computers connected to the Internet. But when information is sent over the Internet to your computer how does your computer accept that information? It accepts that information by using TCP or UDP ports.

So, you have one device with an IP address, and each IP address has a large number of ports associated with it, with a total of 65,535 TCP Ports and another 65,535 UDP ports (to help visualise this, think of your IP address as a train station, and each of the ports as a platform within the train station, which receives incoming "trains" of data). When your game sends data over the internet it sends that data to an IP address and a specific port on the remote server, and it receives data on a (usually) random port on its own device. If it uses the TCP protocol to send and receive the data then it will connect and bind itself to a single TCP port for the duration of the connection, but if it uses the UDP protocol to send and receive data, it will use any available UDP port.

When choosing a port to bind to, in general lower values will already be in use by other programs so try to use a high value (you can find a list of ports and general usage here).

NOTE: Once an application binds itself to a particular port, that port can not be used by any other application. It is first come, first served.

The final argument, max_client is the maximum number of client devices that connect through this port. This value will depend on your game, but too many connected clients will saturate the network or the device CPU won’t be able to handle the processing of that number of players, so try to balance the number of clients with the amount of data being sent (large amounts of data = lower number of clients) and keep the connections to the minimum needed for your desired gameplay.

NOTE: When your server has connected to a maximum number of clients, any further client trying to connect will not be able to, but there is no error given for this. You will need to code your own "failsafe" system to catch this (see the "Creating A Client" section, below).

Here is an example code for setting up the server:

server_socket = network_create_server(network_socket_tcp, 6510, 4);
if server_socket < 0
    {
    //Connection error! Add failsafe codes here
    }

As you can see from the code above, when you create a server, the function returns a value with anything from 0 and over meaning that the server has been created, and a value of less than 0 is an error. Errors can be dealt with in a number of ways, for example using the while function to loop through the ports to find an open one, or by setting an alarm to retest the same port (or a different one), or even by telling the user that there is no connection and request that they retry.

That's the server set up, but what about the client? The next section covers how to set that up...

Creating A Client

So we have created the server socket, ready to receive incoming client connections, so we should now create those clients. Like with the server, the initial setup is done with a single function:

network_create_socket(type);

The type here (as with the server) can be either TCP or UDP, and the same constants that you used for the server are applicable here (network_socket_tcpnetwork_socket_udp).

NOTE: The client socket must use the same protocol (TCP or UDP) as the server socket!

That function will create a socket on the device ready to send and receive data, retunring a value that should be stored in a variable to identify this socket in future function calls. We now need to tell it where to connect to, and for that we have the following function:

network_connect(socket, url, port);

Here the socket is what we want to connect to our server through, and it should be a variable holding the the returned value that we got when we created it previously. The url is the direction (IP) of the server that we want the client device to connect to (this is a string with the format “xx.xx.xx.xx”, and the port to connect to.

Here is a brief example of how this would be coded:

client_socket = network_create_socket(network_socket_tcp);
var server = network_connect(client_socket , ”127.0.0.1”, 6510);
if server < 0
    {
    //No connection! Failsafe codes here...
    }
else
    {
    //Connected!
    }

With that done and a connection established between the client and the server, we are now ready to create a connection and start sending data packets.

Detect Client Connection

To detect client/server data transfers we now have to use the Asynchronous Network Event. We use an asynchronous event because you have no idea when data will come in as it depends on network speeds and load etc..., so we use this Network event to receive all data including connect/disconnect details.

So how do we detect a connection? Well the async Network event will generate a special ds_map for this event (which is automatically deleted at the end of the event, so don't try and access it anywhere else). This map contains a number of key/value pairs that can be checked to get information on the type of network data received, with the following values being common to all network events:

"id" - The identifying value of the socket receiving the data.
"ip" - The IP address of the connecting socket (as a string).
"type" - this returns the type of network event being triggered and can be one of three constants (see the table bleow)

The following table shows the constants accepted for the event type being triggered:

 Type Constant  Description

 network_type_connect

 The event was triggered by a connection.

 network_type_disconnect 

 The event was triggered by a disconnection.

 network_type_data

 The event was triggered by incoming data.

When we want to check for a connection, we also have an additional key/value pair in the async_load ds_map:

"socket" - the socket id of the connecting/disconnecting device.

So if we wish to check for a client connection to our server we would have something like the following code:

var n_id = ds_map_find_value(async_load, "id");         //get the id of the socket receiving the data
if n_id == server_socket                                //check id to make sure it is that of the server socket
    {
    var t = ds_map_find_value(async_load, "type");          //get the type of network event
    if t == network_type_connect                            //if it is a connect event 
        {                                                   //get the socket id of the connection
        var sock = ds_map_find_value(async_load, "socket"); //and store it in a variable
        ds_list_add(socketlist, sock);                      //then write it to a ds_list for future reference
        }
    }

The above code checks the async_load map for the "id" key, then compares that to the socket id that we stored when we set up the connection. If the id is the same as the server, the map is then checked to see what kind of event is occurring, in this case it is a connection, and the connecting socket id is stored to a ds_list that we would have previously created for this purpose. Why a list? Well, we are making a multiplayer game so we need to store the individual socket id of every connecting device on the server, and a ds_list is the most efficient way to do this.

What about disconnecting? And receiving data? All this can be handled easily by making a simple change to the above code to check the "type" key of the map:

var n_id = ds_map_find_value(async_load, "id");
if n_id == server_socket
    {
    var t = ds_map_find_value(async_load, "type");
    switch(t)
        {
        case network_type_connect:
            var sock = ds_map_find_value(async_load, "socket");
            ds_list_add(socketlist, sock);
            break;
        case network_type_disconnect:
            var sock = ds_map_find_value(async_load, "socket");
            ds_map_delete(socketlist, sock);
            break;
        case network_type_data:
            //Data handling here...
            break;
        }
    }

Connect Client To Server

Connecting your client device to the server is also done from the Asynchronous Networking Event, with all the data being stored in the async_load map. however, unlike the server side code, this is much simpler as all you have to do is check for incoming data (we do not need to check for a connection since if you are not connected to a server you can't receive anything from it):

var n_id = ds_map_find_value(async_load, "id");
if n_id == client_socket
    {
    //We have a new packet from the server
    } 

Our client will "listen" for incoming data and when it is received, the event will trigger and the code will check the id of the socket receiving the data. If that id is the same as that of the socket we created for networking, then we can go ahead and process the data received.

Sending Data

Essentially the sending of data is the same for both the client and the server, with only very minor differences. However, you should note that for the networking functions you will need to have a working knowledge of the GameMaker:Studio Buffer Functions, since the data "packets" that we are going to be sending are made up of raw data taken from a pre-filled buffer. If you are not familiar with these functions, then please see the page on Buffers in the manual before continuing.

So, to send data we first need to write it to a buffer, and then send the buffer over the network as a packet of data. It is worth noting that generally you would want to define a series of custom constants to use when sending data over the network, and you would add them to the first byte of the send buffer before the actual data itself. This enables you to parse the incoming data easily, as you can then check the first byte of the buffer packet to see what type of data to expect and vary your code accordingly.

The following example illustrates a typical send from a server to a client device:

var t_buffer = buffer_create(256, buffer_grow, 1);
buffer_seek(t_buffer, buffer_seek_start, 0);
buffer_write(t_buffer , buffer_u16, CONSTANT);
buffer_write(t_buffer , buffer_string,”Hello”);
//More data here...
for (var i = 0; i < ds_list_size(socketlist); i++;)
    {
    network_send_packet(ds_list_find_value(socket_list, i), t_buffer, buffer_tell(t_buffer));
    }
buffer_delete(t_buffer);

Here we have created a temporary buffer and added the data we need to send, using a grow buffer to ensure that there are no buffer overflow errors (these occur when you write data past the end of a normal buffer). We then loop through the ds_list that the server has containing all of the connected sockets and send out the data "packet" to each of them. Note that we use the function buffer_tell() to get the current position of the "tell" (the read/write position) of the buffer - since it will always move to the end of the last piece of data to be written to the buffer, it is a an excellent way to get the exact size of the buffer and ensure that our data packet isn't padded with unnecessary bytes from empty buffer space.

The process for sending from the client to the server is almost exactly the same as that shown above, only now instead of sending to multiple sockets we only send to one (the socket id that we stored when we created the socket usingnetwork_create_socket).

var t_buffer = buffer_create(256, buffer_grow, 1);
buffer_seek(t_buffer, buffer_seek_start, 0);
buffer_write(t_buffer , buffer_u16, CONSTANT);
buffer_write(t_buffer , buffer_string,”Hello”);
//More data here...
network_send_packet(client_socket, t_buffer, buffer_tell(t_buffer));
buffer_delete(t_buffer);

We have now sent out information from our client to our server and vice-versa, but what about receiving that data? The next section covers this...

Receiving Data

The method of receiving data from a socket connection is simple, and the following instructions cover both client and server. All incoming data will trigger an Asynchronous Network Event, which will in turn generate a special ds_map which can be accessed using the built in async_load variable. This map will be deleted at the end of the event, so do not try to access it outside of the Network Event!

So, to detect incoming data and act on it, we would have something similar to the following in the Network Event:

var n_id = ds_map_find_value(async_load, "id");
if server_socket == n_id
    {
    var t_buffer = ds_map_find_value(async_load, "buffer"); 
    var cmd_type = buffer_read(t_buffer, buffer_u16 );
    var inst = ds_map_find_value(socket_list, sock );
    switch (cmd_type)
        {
        case KEY:
            //A key has been pressed so read the keypress data from the buffer
            break;
        case HEALTH:
            //The player has taken a hit so remove health from them
            break;
        //etc...
        }
    }

The above code is just a rough outline of how things can be done, obviously in your games the details will be different, but you can see the basic steps that should be taken.

The first thing we do is check to see that the data incoming belongs to that of the server socket, and then we can continue to set a variable to access the buffer data that has been stored in the async_load map (since this buffer has been created specially within the async_load ds_map, GameMaker:Studio will automatically delete it at the end of the event, so you don't need to worry about that). We then store the first bytes of the buffer to get the command constant that we have sent to identify what "type" of information has been received, and get the id of the sending socket.

Now that we have this information, it is a simple case to use a "switch" to check the type of information being received, and in each different case we would continue to parse the buffer to extract information and associate that with the id of the incoming socket. For example you could have a series of arrays to hold the instance associated with any given socket and it's position, key-state, health etc... then set each of these values from the buffer data.

Note that the above code is a basic overview of receiving data for both the client and the server, the only difference being how you handle the incoming data (for the server you are updating all client positions, health etc... and for the client you are updating the other clients information for rendering).

Have more questions? Submit a request

0 Comments

Article is closed for comments.