Sei sulla pagina 1di 9

Introduction To Networking In GameMaker Studio 2

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.
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.
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.
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:
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.

Potrebbero piacerti anche