Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more

Tech

Introduction To Networking In GameMaker Studio 2

Networking_techblog

Posted by Mark Alexander on 21 November 2017

This tech blog is a revision of the Introduction To Networking blog post, only updated for use with GameMaker Studio 2. In this blog we're going to give a brief overview of what networking is and how it can be achieved in GameMaker Studio 2. It is worth noting that to accompany this tech blog you can get a simple LAN Platformer Demo from the YoYo Games Markletplace. This demo shows the concepts that we'll discuss being used in a real game for you to study and learn from.

Sockets

To start with, let's talk about sockets. The actual definition of a socket is "one endpoint of a two-way communication link between two programs running on the network", so for a game being run on two different devices to talk to each other, each will need to have a socket connected to the other. We'll be doing this initially using TCP/IP connections, which is what the internet is based on (IPv4 to be more precise). This in essence will let you connect one socket using an IP address (let’s say 192.168.1.10) to another socket on another IP address (say 192.168.1.11). Once connected, the two sockets can send data back and forth using the network_send_packet() function and the Network Asynchronous Event in GameMaker Studio 2, but we’ll get more into that later in the tech blog.

Note: Sockets and networking are available on all platforms excluding HTML5.

IP addresses also have what are called ports. Instead of programs having to read and deal with every bit of network traffic coming into a device, IP addresses are linked with ports which are given a value ranging from 0 to 65535. This allows each program to "listen" to only a specific port number to get just the packets that are relevant to it, and not everything that's coming in. This saves considerable CPU time, as it seriously cuts down on data processing. One thing you should note though, is that some ports are already in use by your system, and so you should try to pick an appropriate port number for your sockets. You can see a general list of port values and their uses http://en.wikipedia.org/wiki/ListofTCPandUDPportnumbers.

Sockets Visualisation

So to reiterate over what's been said: A socket is a connection point bound to a port on an IP address. It can be either a client socket - and so can send and receive data - or it can be a server socket and “listen” to a port to get connection/disconnection information as well as send and receive data. This means that the typical flow of events in a game would be:

  • create a server socket
  • tell it to listen to a port
  • have a client try to connect to the socket
  • the server connects and creates a “link” between the client and server sockets

Once the link has been established the game can then freely send data back and forth.

Networking Asynchronous Event

An important thing to note about networking is that everything happens asynchronously. This means that you can control when you send data out over the network, but have no idea when data will come in. So, you can send a data packet to the server but the game will continue to run until the server replies and returns data, which could be several milliseconds later, meaning that we have to have some way to intercept this data within the GameMaker Studio 2 event system. That's where the Asynchronous Events come in...

Async Events are an event category that has no fixed position in the regular order of events, and the events in this category will be triggered at any time if any incoming data is received. In the case of networking, there is a specific Asynchronous Network Event that will be triggered when a connection or disconnection is detected or when incoming data is received. What this means is that a game can send data out from any event at any time, but that all incoming data must go through the Async Networking event, for both client and server. This means that we need to use our Networking event to find out what the incoming data is for and what it contains, then act accordingly.

Networking Async Event

Before continuing it's worth noting that GameMaker Studio 2 will do a few things in the background when you are receiving data over the network. Generally, sockets send data “streams”, meaning that if a device sends 5 data packets over the network to a server, the server may end up getting only one single block of data comprised of all 5 packets "merged" into one. So rather than 5 callbacks of 32 bytes, you get one callback of 160 bytes. This can make networking a bit more complicated, but thankfully if you are using the network_send_packet()function, then GameMaker STudio 2 will automatically split these combined packets up. What GameMaker Studio 2 does is attach a small "header" to each packet sent so when it is received it knows it’s a packet, and its size, and will trigger a separate Async event for each one so you can process it individually .

Writing A Networked Game

So, exactly how should you write a simple networked game? Well, there’s obviously an infinite number of ways, so we’ll pick a very simple example, and discuss that. For our system, we’ll run the whole game on the server, leaving the client to just display the results.

Single Server Setup

Now normally in a single player game, you’d have a the player object moving around and checking for keys itself, but for our networked game, we’re going to change that. Rather than the keyboard being directly checked by the player object, we’ll create a new "oClient" object that checks keys, and it will then forward these key pressed/released events to the server as they happen. This is very light on network traffic as, if you are running right (for example), then you get one event to start running, and then much later, one to stop. Only 2 network packets in total, which is ideal, and much better than trying to send an instance position over the network every step,

So, next we’ll need a server, something that will receive these keys and process all the connected players somehow. For that we'll need another controller object "oServer". On creation, our "oServer" object attempts to create a socket and then attempts to listen to port 6510 (in the demo project that accompanies this tech blog we use that port), waiting for a client to connect. We create the server with a single line of code like this in the Create Event:

 server = network_create_server( network_socket_tcp, 6510, 32);

The “32” is the total number of clients we want to allow to connect at once. This number is arbitrary for the demo, but note that with too many connections your game will saturate the network or your CPU won’t be able to handle the processing of that number of players, so set this value with care. Note that if creating the server fails, then it may be that we already have a server on the device, or that the port is in use by another program. Only one socket can listen to the same port at once, so if something else is using it, then you’ll need to pick another. you can add in some error handling code here to loop through a set of port values (for example) or simply not connect and run the game offline.

Once our server is created and listening, we can then get our client to connect. For that we'll need to create a socket in the Create Event, then use that to try to connect:

client = network_create_socket(network_socket_tcp);
network_connect(client, “127.0.0.1”, 6510);

NOTE: “127.0.0.1” is a special network address that is ONLY your device. It’s a “loopback”, meaning nothing actually goes out on the network, but is instead delivered directly back to your own device. This can be changed later if required.

And that’s it for setting up the client and server sockets! We are now ready to send data back and forth between them.

Sending Data

The first thing the client needs to do now is to send a special packet to the server, telling it the players name. To do this we use a buffers to create a packet of raw, binary data that we can send to the server. If we go into our "oClient" object, we can create a new buffer in the Create Event:

buff = buffer_create( 256, buffer_grow, 1);

This creates a new buffer 256 bytes in size, that will grow as needed, with an alignment of 1 (no spaces left), which - for our minimal traffic - is just fine.

NOTE: We recommend you don't create and destroy the buffer for each package, but rather create the buffer once and then keep it around so that you can reuse it. Keep in mind that you'll need to reset the read/write position back to the start each time (see buffer_seek() in the manual for more information).

To send some data to the server we simply have to write it to the buffer, and send it.

buffer_seek(buff, buffer_seek_start, 0);
buffer_write(buff, buffer_s16, NAME_CMD);
buffer_write(buff, buffer_string, player_name);
network_send_packet(client, buff, buffer_tell(buff));

And that’s it. The buffer_seek() at the start sets the buffer write position to 0 and then we write some data: first a constant to tell the server what type of data the packet contains, and then a string with the player name - and then we send the buffer packet out over the network.

Connecting / Disconnecting

How will the server get the data that the client sends out? As already mentioned, we'll be using the Asynchronous Network Event in the "oServer" object:

NetworkEvent

The Async Network event creates a new ds_map and assigns async_load to hold it, and this allows us to look up everything we need, and this lets us decide on the current course of action.

NOTE: `asyncload`is a DS map that GameMaker Studio 2 generates automatically for you in all Asynchronous events. outside of these events it is set to a value of -1._

var eventid = async_load[? "id” );

Here we get the unique socket ID of the socket that threw the networking event. This can either be the server ID, or an attached client ID. If it’s the server ID then we have a special connection/disconnection event type being triggered, and it’s at this point that we can start creating new players for the attaching client or throwing them away if they disconnect.

So, to tell if it’s a connection or a disconnection, we’ll check the event “type” in the ds_map:

var t = async_load[? "type"];

We can check t now for one of the built-in GML constants, in this case we check to see if it's t == network_type_connect. If it is, then we can get the new socket ID and IP of the connecting device:

if t == network_type_connect
{
 var sock = async_load[? "socket"];
 var ip = async_load[? "ip"];

The variable sock will hold the ID that has been assigned to the connecting client, which will remain the same for as long as the client stays connected. We can therefore use this as a lookup for any client data. We can now store the socket ID sock in a DS map that we'd initialise in the Create Event of our oServer instance, and associate with that socket an instance ID of a new player instance that we create:

 var inst = instance_create_layer(64,192, "Instance_Layer", oPlayer);
 ds_map_add(clients, sock, inst);
}

Now when a client is connected, we store the socket ID along with the instance ID in the DS map, and now, whenever some incoming data arrives from the client, we can simply lookup the instance using the incoming socket ID and then assign the data as needed.

To detect a disconnect we need to check and see if t == network_type_disconnect, then get the ID of the socket, look it up in the DS map, then delete that map entry and destroy the player instance associated with it:

if t == network_type_disconnect
{
var inst = Clients[? sock];
ds_map_delete(Clients, sock);
instance_destroy(inst, true);
}

Receiving Data

The next thing to tackle, is what happens when the client sends some data to the server. This comes in to the Async Network Event with a socket ID that isn’t the server’s, but a client socket that we’ve already connected with and added to our client DS map. This means all we need to do in the server network event code, is check that it’s not the server socket, and if it’s not then lookup the instance ID associated with the socket in the ds_map and start reading the data into there. So, conceptually we'll have with something like this:

var eventid = async_load[? "id"];
// Check the incoming socket ID against the socket ID we stored when we created the server
if server == eventid
{
// Incoming data is from the server so it's a connect/disconnect event and we can deal with it here
}
else if eventid != global.client // Don't deal with data coming from the client running on the device that also runs the server
{
// Deal with received data here from connected clients
}

We've covered detecting connect and disconnect, so what about the part where we receive the data? To start with, we want to get the data from the async_load map:

var sock = async_load[? "id"];
var inst = Clients[? sock];
var buff = async_load[? "buffer"];
var cmd = buffer_read(buff, buffer_s16);

We have the client socket ID, the instances associated with the socket and the packet of data sent in the buffer. We also read the first bytes of data from the buffer as that will hold our event type value so we can tell whether this is a command for the player, or a ping, or the player name, etc... We would then check this in a switch or in an if...else chain and act appropriately, for example:

switch (cmd)
{
case KEY_CMD:
    // Read the key that was sent
    var key = buffer_read(buff, buffer_s16 );
    // And it's up/down state
    var updown = buffer_read(buff, buffer_s16 );
    // translate keypress into an index for our player array.
    if key == vk_left  key = LEFT_KEY;
    else if key == vk_right key = RIGHT_KEY;
    else if key == vk_space key = JUMP_KEY;
    if updown == 0 inst.keys[key] = false else inst.keys[key] = true;
    break;
case NAME_CMD:
    inst.PlayerName = buffer_read(buff, buffer_string);    
    break;
case PING_CMD
    break;
}

Sending Data

The last part of this puzzle is sending out updates to the connected clients, and have it update the game. Again, this is handled through the Async Network Event, and the only difference is that we want to handle it in the client controller object. In the case of the demo that this tech blog is based on, all the client will do is receive the data from the server and use that to draw all the sprites.

NOTE: The server Step Event will sending out this data every game frame, for all the attached players and active enemies. This keeps it simple for the sake of learning, and is actually fine for a small LAN game, but when you expand to a game over the internet this is not the most efficient or workable way to do it. However, that's outside of the scope of this tech blog.

As with the server code, we'll have some code in the client to get the socket ID, the instance associated with that socket and a buffer of data. We can now read the data from the buffer, and store the relevant information in the local instance. So, to start with:

 var eventid = async_load[?  "id"];
 if client == eventid
 {
 }

Now, to buffer the incoming data we opted to use a DS list which in the demo project is created in the oClient Create Event. Whenever we get new data, we clear this list, then add the data to it:

 var buff = async_load[? "buffer"]; 
 sprites = buffer_read(buff, buffer_u32);  // get the number of sprites
 ds_list_clear(allsprites);
 for (var i = 0; i < sprites; i++;)
    {
    ds_list_add(allsprites, buffer_read(buff, buffer_s16));     //x
    ds_list_add(allsprites, buffer_read(buff, buffer_s16));     //y
    ds_list_add(allsprites, buffer_read(buff, buffer_s16));     //sprite_index
    ds_list_add(allsprites, buffer_read(buff, buffer_s16));     //image_index
    ds_list_add(allsprites, buffer_read(buff, buffer_s32));     //image_blend
    ds_list_add(allsprites, buffer_read(buff, buffer_string));  // player name
    }

Now that we have something to draw, we can draw this inside the *oClient Draw Event in a simple loop, and if you check the demo project you can see this being done (and note that the player and enemy objects have their own draw events suppressed with a comment so they aren't drawing twice).

Summary

That's it really! LAN networking is fairly simple, but before we finish let's just revise what we've discussed. At it's most basic all you need for a LAN game to work is a client object, a server object and a player object. These will work together as follows:

  • oClient: The client is a "dumb" client that simply sends key presses/releases to the server, and it will also draw all the sprites from the data that the server sends back.

  • oServer: This is the main controller. It will connect/disconnect clients, create new players as they come in (mapping them to a socket id), and send out all sprite data to all connected clients.

  • oPlayer: The player object is pretty much the same as you'd have in any offline game, except it no longer checks keys directly, but instead checks an array of keys that the server fills in when it gets key press data from the client(s).

Once you have this framework set up it should be relatively easy to expand it to cover other pieces of data, like item pickups, or health, etc... We suggest that you take the Demo that accompanies this tech blog and use it to experiment and add things to. You'll quickly find that basic networking isn't so difficult after all.

Back to Top