In this latest build of GameMaker: Studio, we’ve added some experimental networking, and so I’ll now give you a brief overview of how it all works, what the general rules are, and a way to make a simple client/server that you may use in a game. This will be based on the new Networked Platformer example, so you should glance through it so you have a vague idea what I’m going to talk about.
First, the networking system is based on "sockets"; these are standard, and available on all platforms – excluding for the moment HTML5. We have given you the ability to create both clients and servers in GML allowing you to create even single player games using a client/server model – which will allow multiplayer to be added easily later. I won’t get too far into how “sockets” work, it’s a large subject with LOTS of tutorials, demos, and descriptions on the net, but I will give a very brief overview of what a socket is.
A socket is an object which can send, receive, connect and listen to ports on the network. Initially we’ll deal with just TCP/IP connections, which is what the internet is based on (IPv4 to be more precise). This in essence lets 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, they two sockets can send data back and forth using the network_send_packet() function and the Network ASYNC Event – but we’ll get into that later. First, IP addresses also have whats called PORTS. Instead of programs having to read and deal with every bit of network traffic coming into a machine, IP addresses also deal with ports, ranging from 0 to 65535. This allows each program to get only the packets that it’s interested in, and not everything – this also saves considerable CPU time, as it seriously cuts down on data processing. One thing you should also note, is that some ports are already in use by your sysem, and so you should try to pick a "higher" port number. You can see a list of them HERE.
To make life a little easier, you can also “listen” to ports. So a server will create a socket, and then bind it to a specific port to listen to. It will then get notified of connections, disconnections and data transfers.
So… quick review of a socket; you can connect to a port on an IP address, and read/write some data, while a server can “listen” to a port, and then get connection/disconnection info AND read/write some data. So you create a server, tell it to listen to a port, and when a client tries to connect, the server notices, connects and then creates a “link” between them so that they can freely send data back and forth. Pretty straight forward so far.
Now, this all happens asynchronously. Meaning you send data out, but have no idea when data will come in. To help with this, we have added a new Network ASYNC Event. ALL data is received through this event, along with all connect/disconnect details.
This means we can send anywhere, but all incoming data happens through the event – for both client and server. That means we need to know who the data is for, and what it contains. Now sockets are “streams”, and this means if a machine send 2 data packets to a server, it may end up getting one big block of data in the callback. So rather than two callbacks of 32 bytes, you get one callback of 64 bytes. This makes life a little tricky. GameMaker can help with this. The network_send_packet() call will automatically split these bundles up – you don’t have to, but I'd recommend it. GameMaker attaches a small header to each packet sent so it knows it’s a packet, and the size, and thereby allows it to process each one, handling the annoying stream for you.
So, what you now end up with, is a callback for each packet any client has sent you. This leaves you free to write the server code based on simple packets of data.
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 (and far from perfect!) example, and discuss that. For our system, we’ll run the whole game on the server, leaving the client to just display the results.
Now normally in a single player game, you’d have a simple player object running 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, 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 (say), you get one event to start running, and then much later, one to stop. Only 2 network packets in total, which is ideal.
So, next we’ll need a server, something that will receive these keys and process all the connected players somehow. On creation, our oServer object attempts to create a socket and then attempts to listen to port 6510, waiting for a client to connect (see below). The “32” is the total number of clients we want to allow to connect at once. This number is up to you, too many, and your game will saturate the network or your CPU won’t be able to handle the processing of that number of players – use with care.
server = network_create_server( network_socket_tcp, 6510, 32 );
If this fails, then we may already have a server on this machine – or 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, you’ll need to pick another. Once our server is created and listening, we can then get our client to connect.
First, we’ll deal with a creating a socket, and connecting.
client = network_create_socket( network_socket_tcp );
network_connect( client, “127.0.0.1”, 6510 );
“127.0.0.1” is a special network address that is ONLY your machine. It’s a “loopback”, meaning nothing actually goes out on the network, but delivered directly back to your own machine. This can be changed later.
And that’s it! Once connected, we are now ready to send data back and forth from client to server! The first thing the client does is to send a special packet to the server, telling it the players name. To do this we use one of our new “binary buffers” (please see the manual for details) This lets us create a packet of raw, binary data that we can send to the server. Buffers are pretty simple to use. Simply create one, write some data, and then send it. I would recommend you keep the buffer around so that you can reuse it, remembering that if you do you’ll, need to reset the read/write head back to the start each time. So inside our oClient object, we’ll create a new buffer
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.
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, PING_CMD );
network_send_packet( client, buff, buffer_tell(buff) );
And that’s it. The buffer_seek() at the start allows us to reuse the buffer as the networking system always takes the data from index 0 in the buffer, and throwing buffers away every time is a waste.
So, how does the server get this data? Well using the new Network event, it’s pretty straight forward as well.
We simply pick the networking event from the menu (shown above), and then we can start writing some client/server code for when the data arrives (as shown below).
The 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.
var eventid = ds_map_find( async_load, “id” );
This returns the socket ID that threw the 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, and it’s at this point 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 “type” in the ds_map.
var t = ds_map_find_value(async_load, "type");
If t==1, then it’s a connection, and so we can get the new socket ID and IP it’s come from.
var sock = ds_map_find_value(async_load, "socket");
var ip = ds_map_find_value(async_load, "ip");
The variable sock will hold the ID that has been assigned to that client, and this will be the same as long as the client stays connected. We can therefore use this as a lookup for any client data.
The simplest way of getting client info, is to use a ds_map and use sock as a lookup to an instance which can hold all the clients variables/data. So all you need to do is create a “clients” ds_map in the create event of oServer, and then on connection, the server will create a new player, and add it to a ds_map like this…
var inst = instance_create( 64,192, oPlayer );
ds_map_add( clients, sock, inst );
This then means that whenever some incoming data arrives from the client, we can simply lookup the instance and then assign the data as needed. If it was 0, then we simply have to remove the socket from the map, and delete the player instance.
So the next thing to tackle, is what happens when the client sends some data to the server. This comes in to the Network Event with a sock ID that isn’t the server’s, but one we’ve already connected and initialised. This means all we need to do in the server network event code, is check it’s not the server socket, and if it’s not… lookup the instance in the ds_map, start reading the data into there.
var buff = ds_map_find_value(async_load, "buffer");
var sock = ds_map_find_value(async_load, "id");
var inst = ds_map_find_value(Clients, sock );
So this now gives us the buffer where the client data is, the socket ID it came in from, and the instance attached to that socket; everything we need to read the data and process it. We can now read the data from the buffer, and then store the relevant information in the instance.
The last part of this puzzle is sending out updates to the client, and having it display the game. This is again, pretty simple and handled through a Network Event, and the only difference is that we want to buffer the information coming in, in case there is a connection issue. In the case of our little example, what it does is to simply draw all the sprites sent to it by the server. The server “step” event does this every frame, for all the attached players and active baddies, this keeps it simple for the example.
It should be noted that this is where “smart” client/server models differ from what we have here. They will try to minimise the data being sent, sending out single updates, and limiting how often it transmits. We don’t do anything like that. This system will work fine for a game on a local network, with a few players, but will probably break instantly if you tried this over the internet. Still, with this in mind, how does our simple client receive the data?
First we’d add a network event as we did the server, and then the first thing we’ll check, is that the event ID is the client id – if it is, then it’s for us.
var eventid = ds_map_find_value(async_load, "id");
if( client == eventid )
Now, to buffer the incoming data I opted to use a ds_list, I simply create this in the oClient create event, and then whenever we get new data, we clear it, and add the data to it. Simple.
var buff = ds_map_find_value(async_load, "buffer");
sprites = buffer_read(buff, buffer_u32 ); // Number of sprites
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’s draw event in a simple loop.
var index = 0;
var xx,yy,sp,spindex, col;
xx = ds_list_find_value(allsprites,index++);
yy = ds_list_find_value(allsprites,index++);
sp = ds_list_find_value(allsprites,index++);
spindex = ds_list_find_value(allsprites,index++);
col = ds_list_find_value(allsprites,index++);
name = ds_list_find_value(allsprites,index++);
And that’s it really…. So, let’s quickly recap. We have oClient, oServer and oPlayer.
The oClient is a dumb client that sends key presses to the server, and will draw all the sprites the server sends back.
oServer is the controller; it will connect/disconnect clients, create new players as they come in (and map them to a socket id), and send out all sprites to all connected clients.
oPlayer is just like it was in a single player game (in this case), except it no longer checks keys directly, but an array of keys the server fills in when it gets key presses from the client.