In this article we aim to cover the basics of the Networking functions in GameMaker Studio 2. This article is a basic overview of how to set up a networked game using sockets, and is intended for intermediate users who are already familiar with GML (the GameMaker Language). This article will not tell you how to set up a MMO, and be aware that working with networking on any platform can be very frustrating and you will need time and patience to get it working, especially if it is new to you (even the big companies often sub-contract the networking multi-player parts of their games out to other specialised companies to write!). Generally, don't be too 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 relatively easily using the GameMaker Studio 2 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 the concept of sockets, then it can be quite tricky to set up correctly. This article 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 mail service. If you send a letter, that letter goes along with everyone else's letters to the central mail office via mailmen to be sorted and dispatched to their destination (again, by mailmen), where they are received in the mail box. Well, networking over the internet is the same, with one client sending data "packets" to a server (the mail office) using the internet (the mailman) which will then send it on to the other clients (or back to the original sender) where it's received in the mail box (the socket). Not a perfect analogy, but hopefully it get's the idea across to you!
SERVER TYPES
For any networking to be done using GameMaker Studio 2, 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... This type of setup is most often called peer to peer.
The actual programming 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 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 after you must create a client which should connect itself to the server. This is what we'll be discussing in this article today.
CREATING A SERVER
The socket based networking functions of GameMaker Studio 2 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);
Let's just quickly explain the arguments that this function takes:
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 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 recognise 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, use the mail analogy once more - think of your IP address as a street address, and each of the ports as a path to your front door which receives the mailman with all the "packets" 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 can connect through the given 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 (generally, 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) at a different time, or even by telling the user that there is no connection and request that they retry after a timer has counted down.
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_tcp
, network_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, returning 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
argument 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 then we give 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, the async_load
map (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 below)
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? Both cases 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_list_delete(socketlist, sock);
break;
}
}
As for checking the network_type_data event type, you need a separate block, as the async_load - id value will be different for that event.
The id then will be the socket ID of the client that sent the data, instead of the server's socket ID as in the other event types.
var t = ds_map_find_value(async_load, "type");
if (t == network_type_data)
{
var n_id = ds_map_find_value(async_load, "id"); // socket ID that sent the data
// Data handling here...
}
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 2 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, IDENTIFIER_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 using network_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, IDENTIFIER_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 DS map which can be accessed using the built in async_load
variable. So, to detect incoming data and act on it, we would have something similar to the following in the Async 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 2 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).
SUMMARY
That wraps it up for this article! Hopefully it's given you a firmer grasp of what is required to create a simple, networked peer-to-peer game. We have also created a basic networking test project that builds on the principles outlined here that you can look at. Just import it into GameMaker Studio and then run it on two or more devices. You'll see that you can either set the device to be a server or a client, and connect them together to have two or more players in the game at once.