GameMaker Networking Tutorial

Networking Tutorial: GameMaker Studio

As this tutorial is very GameMaker oriented I will not be going deep into the technological side of networking. This is a tutorial for "intermediate" GameMaker users.

The actual tutorial starts at section "Server Tutorial: Part 1" and goes on from there. All the information before the tutorial is extra, but valuable information!

——————————————————————————————————————-

— TCP / IP / Server-To-Client Model / Client-To-Client Model —

What is TCP?  –> http://en.wikipedia.org/wiki/Transmission_Control_Protocol


What is IP? –> http://en.wikipedia.org/wiki/IP_address

In short TCP/IP works by exchanging packets of information(in the form of bytes) between IP Addresses. These packets of information can be sent / received and used to perform whatever sort of application you might need.

What is UDP? –> http://en.wikipedia.org/wiki/User_Datagram_Protocol

NOTE: This tutorial does not use nor focuses on UDP.
I won't go into UDP, but UDP is similar to TCP, but it has a problem as well as an upside. The upside is UDP is faster than TCP. The downside is that UDP has the possibility of losing packets because UDP is unreliable. UDP is usually used to send unimportant small bits of information, such as retrieving / sending ping. UDP is also connectionless, meaning you don't have a direction connection to anyone or anything with UDP.

TCP works by setting up a host(server) and having a client connect to the host in order to properly exchange information. This can be used multiple ways though, one being Server-To-Client and the other Client-To-Client models.

What is a Server? A server is a host application that stores data and passes all data through itself and sends it to a single client or multiple clients, whichever is necessary.

What is a Client? A client is an application that sends / receives requests from the server it is connected to in order to exchange data with the server.

Server-To-Client: The server to client method works by having clients connect to a host(server) and have the clients send data to and make requests for data from the host(server). This method is more secure than direct client-to-client methods. The reason for this being that each client is not directly connected to each other, but connected to a server instead to act as a medium for data exchange.

Client-To-Client: The client to client method works by having a client host a server ontop of a client and have a client or multiple clients connect directly to the host client. This is less secure than the server-to-client method since clients are directly connected to each other. The reason this is unsafe is that harmful clients can take other clients information and use it without the affected client's permission.

Local Host / Play: (Windows OS Related Only) Local play does not require any extra work besides setting up your game and running the server / client on computers connected to your own router. However if you have Windows Firewall enabled, you'll either need to disable Windows Firewall or allow your program to bypass Windows Firewall. To allow your program through Windows Firewall: Go To Control Panel -> System and Security -> Windows Firewall -> Allow a program or feature through Windows Firewall. Finally, click Change Settings and Allow a Program and find your program. Then hit OK and you're done.

Global Host: Hosting a server for global play—unlike local host—requires port forwarding. I recommend doing "Port-Range Forwarding" where you portforward your IP address on a range of ports. This will allow you to change your port without having to worry about doing another port-forward for your new port.

IPv4 Address Problem: If you're hosting on Windows(not sure about other devices) you may notice your IP address change from time to time. This means your device's IP address is dynamic, meaning it changes. This will in turn make your server's host IP address change as well. To avoid this, what you can do is set your IPv4 Address to a "static IP address". This will keep your IPv4 address from changing and this will keep you from having to port-forward again each time your IPv4 Address changes.

Room Speed: Game Maker Studio processes everything in "steps" which is the cap on the FPS called "room speed". If your room speed is 30, your game runs on average at 30 FPS or 30 steps/frames per second. This limits the number of packets of data your game can process over a network to: X number of messages per 30 steps/frames. So if you increase your room speed, you can increase the number of messages your game's network can process! Example, if your game runs at 500 room speed, your game's network can process up to X number of messages per step at 500 steps per second! See the increase? However having such a high room speed, means you ned to look into delta time: http://en.wikipedia.org/wiki/Delta_timing

——————————————————————————————————————-

Setup: Simple Server / Client Application / Important Information

The rest of this tutorials assumes you already know how to program in Game Maker Studio. Please do not attempt to create a networking application without first knowing how to program or how to program in Game Maker Studio.

— Start of Bonus Info –

Questions and Answers:

What are sockets? Sockets are "identifiers" for clients, each socket points to a specific client and each client is given a socket when it connects to the server. Sockets are used to send data to the clients they are related to to.

* However sockets aren't only for clients on the server. The server and client each have a socket in their own application. The server's socket on the server application is created and returned using "network_create_server()". The client's socket on the client application is created and returned using "network_create_socket()".

  • What are buffers? In short, buffers are a form of data storage. You write data to buffers and read data from buffers. You also read data the same way you write the data. So if you wrote A, B, C in that order, then you'd read the data back out as A, B, C in the same order.

Remember though, when writing or reading data from a buffer, make sure that the data type you're reading/writing matches the value being read from or written to the buffer or you'll end up reading or writing incorrect values!

When dealing with buffers, we can have two buffers. A buffer received from a packet of data sent from a client or server, this is the read buffer. Then we have a second buffer that is used to send data from client to server, or server to client, this is the write buffer.

  • Why do I need to store these sockets? Sockets don't record/store themselves, if you don't have the sockets, you can't send data back to the clients the sockets belong to. That is why we make a data structure to store the sockets in.

  • What is a packet and what is a message ID: A "packet"(in networking) is a collection of data written to a buffer. We can write whatever data we want to a packet, however one thing is always needed for every packet. That is a "message ID" or msgid for short. The msgid of a packet helps the server to determine what the packet is for and how the packet should be processed by the server. The msgid ALWAYS comes before any other data written to a packet. The msgid is also always read from a packet before every other set of data. Message IDs can be whatever you want, a string or real value. However, efficiency is always top priority, so message IDs are usually written to a buffer as a very small "real" data type. A "real" or real value is simply a "number" which can either be an integer or float value(float values are numbers with a decimal point, e.g. 4.5).

Async Networking "Type" Events:

In Any Type Event:
"type" : The type of event.
"id" : The socket receiving the type event.
"ip" : The IP address of the socket receiving the type event.

Network Type Connect:
"socket" : The socket of the client that connected.

Network Type Disconnect:
"socket" : The socket of the client that disconnected.

Network Type Data:
"buffer" : The buffer containing the data we received.
"size" : The size of the buffer containing the data we received.

Data Types For Buffers:

Game Maker has several data types that can be written to and read from a buffer. These data types are listed off and explained below:

Note(s):
The "u", "s" and "f" in the names of the data types stand for: unsigned, signed and float.
The number that comes after the "u", "s" and "f" is the size of the data type in bits.
8 bits = 1 byte. E.g. 8 bits = 1 byte, 16 bits = 2 bytes, 32 bits = 4 bytes, etc.
0 is classified as positive with data types.
bool or boolean is a single byte data type that can be either 0(false) or 1(true).
One character in a string is equal to a single byte.

buffer_u8 : Unsigned integer from 0 to 255.
buffer_u16 : Unsigned integer from 0 to 65,535.
buffer_u32 : Unsigned integer from 0 to 4,294,967,295.
buffer_s8 : Signed integer from -128 to 127.
buffer_s16 : Signed integer from -32,768 to 32,767.
buffer_s32 : Signed integer from -2,147,483,648 to 2,147,483,647.
buffer_f16 : (Not supported!) Float number from -65504 to 65504.
buffer_f32 : Float number from -16777216 to 16777216.
buffer_f64 : Float number from -(2^52) to (2^52) – 1.
-(2^52) = -4,503,599,627,370,496
(2^52) – 1 = 4,503,599,627,370,495;
buffer_bool : A boolean value, a value can only be 0(false) or 1(true).
buffer_string : A string value, size in bytes depend on the length of the string.

Buffer Types:

buffer_fixed : A buffer that is a fixed size and never changes in size. If any data written to the buffer of this type would make this buffer's size exceed it's fixed size, data on the end of the buffer will be removed until the buffer has returned to it's fixed size.

buffer_grow : A buffer that grows as data is added to it. If any data is written to a buffer of this type makes the buffer's size exceed the original size of the buffer, the size of the buffer increases to fit the amount of data stored in the buffer. Buffers of this type will remain the same size as they grow no matter how much data you remove. If your original buffer is size of 1024 and grows to 2048, the buffer's size will remain at size 2048.

buffer_wrap : A buffer that wraps it's data. If any data is written to a buffer of this type that would make the buffer's size increase, the data would then "wrap" the buffer, meaning it'll insert itself at the beginning and overwrite any data it overlaps at the beginning of the buffer. This in turn makes the buffer never increase in size.

buffer_fast : A "stripped" down buffer. This buffer is stripped down, meaning it has little overhead, making this buffer type extremely fast to read/write to compared to other buffer types. However only data types of buffer_u8 and buffer_s8 can be written to this type of buffer. If any data written to the buffer of this type would make this buffer's size exceed it's original size, data on the end of the buffer will be removed until the buffer has returned to it's original size. Estentially, buffer_fast is a faster version of buffer_fixed, however limited in the data types it can hold.

— End of Bonus Info –

— Server Tutorial: Part 1 —
A few things need to be handled before a server is ready to handle any clients or process any information from clients. What we need to do is first create the server via code, create a data structure or array to handle "sockets" that come from connected clients and create a buffer:

//Create Event of Object: ObjServer

var Type , Port , MaxClients;
Type = network_socket_tcp;
Port = 64198;
MaxClients = 32;
Server = network_create_server( Type , Port , MaxClients );

var Size , Type , Alignment;
Size = 1024;
Type = buffer_fixed;
Alignment = 1;
Buffer = buffer_create( Size , Type , Alignment );

SocketList = ds_list_create();

As you can see above, in code, we create a TCP server on port 65535 with a maximum number of connected clients of 32. We also created a ds_list that will hold our client sockets and a buffer with the size of 1024 bytes, the type of buffer is "fixed" and the byte alignment is 1.

As an example and test run, we should start a brand new, empty project. In that project create a new object that will function as our server and give that object a name, e.g. ObjServer. In that object give it a "create event" and type in your code that creates your server. Like the code above. Be sure to type the code in and not just copy and paste it from this tutorial. This will help you to memorize and become familiar with the code.

Now that our server is created we need to check for incoming clients that are trying to connect to the server, remove clients from the server that disconnected and check for data that is being sent from clients. This is all done in Game Maker Studio's Async Networking event.

The async_event has a special ds_map ID that holds all incoming information to the server. This ds_map ID is unique to all the async events, meaning it does not work outside of these events. The ds_map ID for the event is called "async_load". In the async event, the async_load ds_map always holds three pieces of information. These pieces of information are:

Key: "type", key: "id" and key: "ip". Async_load is a ds_map and data in ds_maps are found by searching the ds_maps "keys". The data is stored with these keys and the data can be retrieved via e.g.: data = ds_map_find_value( map , key ).

The rest of the data stored in the map depends on the current event type of the Networking Event. This "event type" is basically what happens when some form of data is received from a client. The event type is in the async_load map as key "type". The event types are located below the "Questions and Answers" section of this tutorial. This next bit of code is a bit lengthy, but please bare with it:

//Async Networking Event of Object: ObjServer

var type_event = ds_map_find_value( async_load , "type" );
switch( type_event ) {
case network_type_connect:
var socket = ds_map_find_value( async_load , "socket" );
ds_list_add( SocketList , socket );
break;
case network_type_disconnect:
var socket = ds_map_find_value( async_load , "socket" );
var findsocket = ds_list_find_index( SocketList , socket );

if ( findsocket >= 0 ) {
ds_list_delete( SocketList , findsocket );
}
break;
case network_type_data:
var buffer = ds_map_find_value( async_load , "buffer" );
var socket = ds_map_find_value( async_load , "id" );
buffer_seek( buffer , buffer_seek_start , 0 );
ReceivedPacket( buffer , socket );
break;
}

Looks complicated right? Fear not, it's actually easy to understand! We start by getting the event type, then we perform a switch statement to check which case(type of event) matches the event found(event_type).

NOTE: You'll notice that the networking event may be a bit strange because of one thing that happens when you're either hosting a server or client. When hosting a client the async_map's key "id" will actually return the "TCP" or "UDP" socket the data was received from. However, when hosting a server the async_map's key "id" will return the socket of the client that sent the data. Be sure to remember this since the GM:S help file does NOT tell you this!

If the switch statement returns type "network_type_connect", then we go ahead and add the client's socket to the "SocketList" to record for later use. If the switch statement returns type "network_type_disconnect", then we check to see if the client's socket is in the "SocketList", if it is in the list, we delete it from the list. If the switch statement returns type "network_type_data", then we get the buffer and the socket ID of the client that sent the data, set the reading position of the buffer to the start. Then pass the buffer and socket to the ReceivedPacket script. I will explain what "reading position" is later on.

Now that the above piece of code is handled, we need to determine what happens to our data/packet in the buffer we received in the "network_type_data" event type. First we know that we passed the buffer containing our data to a script "ReceivedPacket". So if we have not already created the script, we need to create a new script and name it "ReceivedPacket". This script decides what we are going to do with the data/packet. Which brings us to the next piece of code that goes in the ReceivedPacket script:

//Server Script : ReceivedPacket

var buffer = argument[ 0 ];
var socket = argument[ 1 ];
var msgid = buffer_read( buffer , buffer_u8 );

switch( msgid ) {
//Case statements go here…
}

Here you see we are getting the buffer passed to the script, getting the message ID(msgid) of the packet and we're using a switch statement to determine what happens based on the value of the message ID. Of course though, we don't know what data we will be receiving from the client yet, so the switch statement is empty.

Next we add a "game end" event to our server object. Here we want to make sure to delete all dynamic data used by the server. Our code's dynamic data currently includes the server socket, buffer and ds_list. So we want to make sure to delete them:

//Game End Event of Object: ObjServer

network_destroy( Server );
buffer_delete( Buffer );
ds_list_destroy( SocketList );

Extra Info: Now you probably want to do a few small things, such as simply the number of connected clients and check to see if the server was created successfully. You can do this like so:

draw_text( 5 , 5 , "Server Status: " + string( Server >= 0 ) );
draw_text( 5 , 20 , "Total Clients: " + string( ds_list_size( SocketList ) ) );

— Client Tutorial: Part 1 —

Now that the server is taken care of, lets focus on the client and what needs to be done for it to connect to the server and receive data from the server.

First we need a client socket and a code to connect that client socket to a server. Of course you can only connect to a server if it is online(but that's implied right?). Next we need a buffer to send our data/packets through for when we send messages to the server:

//Create Event of Object: ObjClient

var Type , IPAddress , Port;
Type = network_socket_tcp;
IPAddress = "127.0.0.1";
Port = 64198;
Socket = network_create_socket( Type );
isConnected = network_connect( Socket , IPAddress , Port );

var Size , Type , Alignment;
Size = 1024;
Type = buffer_fixed;
Alignment = 1;
Buffer = buffer_create( Size , Type , Alignment );

This code here will create our server, create buffer to send data/packets on and check to see if the client actually connected to the server! First we need to get the socket type, in this case TCP, then the server's IP address, we're using localhost, and then we need the port the server is hosted on. After that we create our buffer, just like we did on the server.

As an example and test run, we should start a brand new, empty project. In that project create a new object that will function as our client and give that object a name, e.g. ObjClient. In that object give it a "create event" and type in your code that creates your client. Like the code above. Be sure to type the code in and not just copy and paste it from this tutorial. This will help you to memorize and become familiar with the code.

Now that we have created our client we need to check for any incoming data/packets from the server. Since clients don't receive connection/disconnection type events, we don't include them in our client side code for the Async Networking Event:

//Async Networking Event of Object: ObjClient.

var type_event = ds_map_find_value( async_load , "type" );
switch( type_event ) {
case network_type_data:
var buffer = ds_map_find_value( async_load , "buffer" );
buffer_seek( buffer , buffer_seek_start , 0 );
ReceivedPacket( buffer );
break;
}

We start by getting the event type, then we perform a switch statement to check which case(type of event) matches the event found(event_type), since we're only checking for incoming data/packets from the server, we only check for one event type: network_type_data. If the switch statement returns type "network_type_data", then we get the buffer the server sent, set the reading position of the buffer to the start then, pass the buffer to the ReceivedPacket script. I will explain what "reading position" is later on.

Now we need to determine what happens to our data/packet in the buffer we received in the "network_type_data" event type. First we know that we passed the buffer containing our data to a script "ReceivedPacket". So if we have not already created the script, we need to create a new script and name it "ReceivedPacket". This script decides what we are going to do with the data/packet. Which brings us to the next piece of code that goes in the ReceivedPacket script:

//Client Script : ReceivedPacket

var buffer = argument[ 0 ];
var msgid = buffer_read( buffer , buffer_u8 );

switch( msgid ) {
//Case statements go here…
}

Here you see we are getting the buffer passed to the script, getting the message ID(msgid) of the packet and we're using a switch statement to determine what happens based on the value of the message ID. Of course though, we don't know what data we will be receiving from the server yet, so the switch statement is empty.

Finally we add the "game end" event to our client object. Here we want to make sure to delete all dynamic data used by the client. Our code's dynamic data currently includes the client socket, buffer and ds_list. So we want to make sure to delete them:

//Game End Event of Object: Client

network_destroy( Socket );
buffer_delete( Buffer );

Extra Info: Now you probably want to do a few small things, such as display if the client is connected to the server or not:

draw_text( 5 , 5 , "Client Connected: " + string( isConnected ) );

— Server & Client Tutorial: Part 2 —

You've gotten this far, might as well finish the tutorial! So far we have covered setting up a simple client and server. This includes the server being able to check for connecting / disconnecting clients and receiving data from clients. It also includes the client being able to connect to a server and be able to receive data from a server. However we're still missing one piece of crucial information! Sending data/packets as well as identifying data.packets we retrieve in our ReceivedPacket script on the server and client.

First we know that data/packets are written to buffers. Thus, we'll take a short walk through buffers and how to set them up as packets for networking.

As networking needs to be both efficient and concise to support a game or application, we need to also keep our buffers efficient and concise. This means that we should only write data that is *completely necessary* to the buffer.

For this next part, we'll be setting up a simple but effective *pinging* example using the code we created in the tutorial. If you didn't know, pinging(in networking) is a process by which we check the validity or strength of a connection between two applications, such as a server and client.

You can do two simple things with data in a buffer, read data from a buffer or write data to a buffer. The "reading position" as mentioned earlier, is the position in the buffer we want to start reading data from. The "writing position" is the position in the buffer we want to start writing data to the buffer.

For networking(with games) what we want to do is always start reading / writing data from the beginning of the buffer, this makes using buffers very easy to handle as the process is very linear. So first off, we want to set the writing position to the start of the buffer, then simply write our data to the buffer. On that note, lets write our first packet that our client will send to the server in order to request a ping of the server:

//Step Event of Object: ObjClient

buffer_seek( Buffer , buffer_seek_start , 0 );
buffer_write( Buffer , buffer_u8 , 1 );
buffer_write( Buffer , buffer_u32 , current_time );
var Result = network_send_packet( Socket , Buffer , buffer_tell( Buffer ) );

Despite the code being short, we did a LOT right here. First we set the writing position of the buffer to the start, we wrote our packet's "message ID"(as mentioned earlier) to the buffer and then wrote the current time to the buffer. Then finally, we sent the buffer/packet of data to the server from the client and got the result. Getting the result of the send allows us to check if we're still connected to the server! If the result is greater than or equal to zero, then the buffer/packet of data was successfully sent, else the send failed. "Socket" is the socket that we're connected to and where we're sending the buffer. "Buffer" is the buffer containing our packet. "buffer_tell( Buffer )" is the size in bytes of the packet of data we wrote to the buffer.

At this point, if you haven't started wondering already, I'll bring the question up for you: "Why do we not write the code in the Async Networking Event?" with which I answer: The Async Networking Event is not optimal for sending data unless, we're sending data right after we've received data. The reason being, the Async Networking Event is only active when either a client connects to a server, a client disconnects from a server or a server or client is receiving data. When none of these cases occur, we wouldn't be able to send data, thus we send our data in the "Step Event" since the data is independant of reading data from a buffer.

Now you know how to change the writing position of the buffer, write data to the buffer and how to send data between server and client. So lets get into reading from a buffer. This is relatively simple, we set the reading position(same way as writing) to the start of the buffer and read our data from it. Since earlier, we sent a packet, to the server, we'll retrieve it in our ReceivedPacket script:

//Server Script : ReceivedPacket

var buffer = argument[ 0 ];
var socket = argument[ 1 ];
var msgid = buffer_read( buffer , buffer_u8 );

switch( msgid ) {
case 1:
var time = buffer_read( buffer , buffer_u32 );
buffer_seek( Buffer , buffer_seek_start , 0 );
buffer_write( Buffer , buffer_u8 , 1 );
buffer_write( Buffer , buffer_u32 , time );
network_send_packet( socket , Buffer , buffer_tell( Buffer ) );
break;
}

As you'll notice we update the Server's ReceivedPacket script with our new code. What we did was retrieve the buffer, read our message ID from the buffer and determined what we should do depending on the message ID. In this case our message ID is 1, so we perform case 1. In case 1 we read the current time out of the read buffer, write our message ID to the write buffer and write the current time into the write buffer and send the packet of data to the client via "socket".

Then do the same process for the client, but we won't be sending anything back to the server this time. We'll be doing *pinging* which is calculating the strength of the connection between client and server:

//Client Script : ReceivedPacket

var buffer = argument[ 0 ];
var msgid = buffer_read( buffer , buffer_u8 );

switch( msgid ) {
case 1:
var time = buffer_read( buffer , buffer_u32 );
var Ping = current_time – time;
break;
}

So now you'll notice, that, Ping = Current Time – Previous Time. So Ping is the time it takes for the client to send a packet to a server and for the server to send a packet back to the client!

With this, we have covered all bases! We know how to set up a client and server. How to check for connections / disconnects of clients on a server and how to send / receive data between server and client.

Extra Info: If you'd like to display data such as the "Result" or "Ping" you'll need to initiate their variables in the ObjClient's create event and remove "var" from before the names of both variables. You can then display them on screen:

draw_text( 5 , 5 , "Connected: " + string( Result >= 0 ) );
draw_text( 5 , 20 , "My Ping: " + string( Ping ) );

Remember though, previously we had "isConnected" which was our original way to check if we connected to the server once the client connected. However, "Result" updates if we're *still* connected or not. So keep this in mind since both are not needed.

—————————————————End of Tutorial———————————————-

————————————————-Buffer Explanation——————————————-

NOTE: Data types have been included at the top of the tutorial under section “— Start of Bonus Info —” in “Data Types For Buffers”.

As you will notice networking is completely dependant upon buffers. Buffers are needed in order to both send data and receive data; so it's necessary that we have a good understanding of buffers!

Lets recap what buffers do and how they're used in networking: Buffers are forms of data storage that can be used in plenty of different ways. You can use buffers to store data, send or receive data over a network or even for encoding game save data or decoding game load data.

In order to used buffers we need to know their functions, so lets go over a few very useful ones. The first being: “buffer_create( size , type , align )”. This functions will allocate(apply) dynamic memory for the buffers use, the amount of memory allocated depends on the "size” of the buffer, once the buffer is created the function returns the buffer’s ID. A buffer needs a “type”, there are multiple types of buffers you can create: buffer_fixed, buffer_grow, buffer_fast and buffer_wrap; these buffer types have been explained in the “— Start of Bonus Info —” in “Buffer Types” section of the top of the tutorial. It would be advantagous to look over each buffer type so you can get a better understanding of how each type works. Finally we have “align” or “byte alignment”. This is a tricky subject…

Now networking isn’t exaclty dependant upon having a byte alginment higher than 1. In turn this means you don’t need to learn about byte alignment unless you’re interested. If you arei nterested you can look it up here: http://gmc.yoyogames.com/index.php?showtopic=602321&hl=

So to create a simple buffer, this will create a “fixed” type buffer with a size of 1024 bytes with a buffer alignment of 1(example):

Buffer = buffer_create( 1024 , buffer_fixed , 1 );

Now that we have our buffer, we want to know where to start writing data to it. To do this we have our next function: “buffer_seek( buffer , base , offset )”. This function takes your “buffer” and changes the writing(and reading) position in the buffer. So “buffer” is the buffer to change positions of, “base” is the position to change to and “offset” is the offset of the intial position in bytes. “base” however has three constants you can use: buffer_seek_start, buffer_seek_end and buffer_seek_relative. “buffer_seek_start” sets the writing / reading position to the start of the buffer. “buffer_seek_relative” sets the writing / reading position relative to the current writing(and reading) position. “buffer_seek_end” sets the writing / reading position to the end of the buffer.

As an example we can set the writing / reading position in our previous buffer to the start of the buffer like so:(example)

buffer_seek( Buffer , buffer_seek_start , 0 );

We have now set our writing position of our buffer so lets work on writing data to the buffer using: “buffer_write( buffer , type , value )”. This function takes your “buffer” and writes a piece of data of type “type” with a value of “value” to your buffer. After the value is written, the writing / reading position in the buffer is advanced by the number of bytes written to the buffer with this function. This allows you to write multiple values to a buffer one after another without hassle. 

For example lets write a buffer_u8(8bit or 1byte) value to our buffer(example):

buffer_seek( Buffer , buffer_seek_start , 0 );
buffer_write( Buffer , buffer_u8 , 16 );

Writing data is easy, now reading data is just as easy using: “buffer_read( buffer , type )”. This function reads a piece of data of data type “type” from your :buffer”. After the value is read from the buffer, the writing / reading position in the buffer is advanced by the number of bytes read from the buffer with this function.

So lets try reading a value from out buffer, that we wrote to the buffer previously:(example)

buffer_seek( Buffer , buffer_seek_start , 0 );
var value = buffer_read( Buffer , buffer_u8 );

So what if we want to get the size of all the data in the buffer after we have written the data to the buffer? We use: “buffer_tell( buffer )”. This function gets the total number of bytes of data written to the buffer after the writing position. So if we set the writing to the start and write 4 buffer_u8 data types to the buffer, this function we return 4 bytes(4 buffer_u8 types).

In networking we want to only send the amount of data we have written to our buffer from the current seek position, buffer_tell( buffer ) helps us with this! As the function gets the total amount of data written after our writing position and we can then relay that to sending our packet via: network_send_packet( socket , buffer , size ). See the “size”? That is where buffer_tell( buffer ) would go in order to send only the data we want to send as previously stated.

So lets try getting the total number of bytes written to the buffer(example):

buffer_seek( Buffer , buffer_seek_start , 0 );
buffer_write( Buffer , buffer_u8 , 16 );
buffer_write( Buffer , buffer_u8 , 16 );
buffer_write( Buffer , buffer_u8 , 16 );
buffer_write( Buffer , buffer_u8 , 16 );
var total = buffer_tell( Buffer );

On some occasions you might want to get the size of your entire buffer. You can do this using: “buffer_get_size( buffer )”. Please note that depending on the type of buffer you’re using the size of your buffer never changes(only grow types change). So if you are not using a grow type buffer, adding data to your buffer will never change the initial size of the buffer.

Getting the size of the buffer is as simple as this(example):

var buffersize = buffer_get_size( Buffer );

On other occasions you might want to get the size in bytes of a specific data type. You can do this using: “buffer_sizeof( type )”. This function will return the size of any data type in bytes.

You can use it like so(example):

var sizeof_type = buffer_sizeof( buffer_u8 );

Now, another important thing to do with buffers is to delete them. This is because buffers use dynamic memory. Meaning that the buffer will remain in memory when you're done using it if you don't delete it! All dynamic memory must be deleted or it'll stack up and cause your game to run out of memory and collapse in on itself. To delete a buffer you can use: "buffer_delete( buffer )".

Deleting a buffer is as simple as:

buffer_delete( Buffer );

This concludes basic use of buffers for creating, reading, writing and other simple tasks using buffers.

————————————————–Data Structures———————————————-

If you didn't know, data structures are crucial to handling data with networking. For example in this tutorial, we create a ds_list or (d)ata (s)tructure list to store our client's socket IDs when they connect to the server. So let us walk through data structures.

Just like buffers, data structures are another form of data storage. There are even multiple types of data structures: ds_map, ds_list, ds_grid, ds_queue, ds_priority and ds_stack. That is a lot to cover, however we will only be covering ds_lists and ds_maps as these are pretty easy to understand.

First though, let me point out that data structures—like buffers—use dynamic memory.

To start off, lets learn about ds_list. A ds_list lets us store a basic list of data where each piece of data is given a position in the list to which we can reference back to the data later on. So let us create a ds_list and add data to it, reference the data back and finally delete it:

You can create a ds_list like so:

MyList = ds_list_create();

The above code will simply create your ds_list and return it's ID into a variable which we can use to call the ds_list later on to add or reference data from, easy right? So let us go ahead and add some data to our new ds_list. We can do this via ds_list_add( list , value ) like so:

ds_list_add( MyList , 16 );

Simply enough? This will add value 16 to our ds_list, MyList. Now when you add data to a ds_list, the ds_list puts the data at the end of the list. So if you have no data in your list, then the value added(in this case 16) will be added to the first position in the ds_list. The first position in a ds_list is 0. All data added after will be added chronologically at positions: 1, 2, 3, 4, etc. So now that our data is added, let us find our data in our ds_list, MyList. Since no data was added before our value of 16 then our value is added at position 0. So we'll need to get(or reference) the value back from position 0 using "ds_list_find_value( list , position )" like this:

var value = ds_list_find_value( MyList , 0 );

As you'll notice the code above find(or refences) our value at position 0 that we added into the ds_list, MyList, and returns it to the variable "value". Sometimes you may not want to only find and add data to a ds_list, but even "insert" data into a ds_list. Insert meaning, take your value and add it to a specific position in a ds_list. However, doing so takes all data at and after the position the value is being inserted to and moves the data up one position. Example: If you have values: 2, 4, 6 and 8 at positions: 0, 1, 2 and 3, inserting value 16 at position 2, would move values 6 and 8 from positions 2 and 3, to positions 3 and 4. Easily enough we can insert data into a ds_list using "ds_list_insert( list , position , value )" like so:

ds_list_insert( MyList , 0 , 8 );

Sometimes you might even want to delete a value from a position in a ds_list. For example, we might want to delete our value 8 from our position of 0 in the ds_list using
"ds_list_delete( list , position ) " like this:

ds_list_delete( MyList , 0 );

Finally when we're done using our ds_list as in, we no longer need it, we need to delete the ds_list from the dynamic memory. This can be done using "ds_list_destroy( list )" like so:

ds_list_destroy( MyList );

There is a lot more information on ds_lists, such as more functions for them in the Game Maker help file, so make sure to check that out. This concludes basic use and information on ds_lists.

Moving on, we next have ds_maps. Unlike ds_lists, ds_maps do not use positions to find data, but rather, "keys" to find data. A "key" is a user defined reference to a piece of data. FOr example if we had a piece of data representing our server's IP address, we could store the IP address under a key in a ds_map then later get the IP address back again using the key.

Let us first create our ds_map using "ds_map_create()" like so:

MyMap = ds_map_create();

Like creating a list, the above function creates our ds_map and returns it's ID into the variable MyMap for which we can use later to reference back to the ds_map. Now you might want to add a value to a ds_map under a specific key, you can do this using "ds_map_add( map , key , value )" like below(using the previous example explained):

ds_map_add( MyMap , "Server IP" , "127.0.0.1" );

Easily enough, the above code adds our IP address "127.0.0.1" to a key our key i nthe ds_map "Server IP". Now that we have added that to our ds_map, MyMap, lets get the IP address back using the key with function "ds_map_find_value( map , key )" like so:

var ipaddress = ds_map_find_value( MyMap , "Server IP" );

ds_maps do not deal wth positions, so their is no finding values by positions or inserting data at specific positions like ds_lists. Thus we can simply find and add values but not insert. Now we might want to delete our key and it's value from our ds_map, MyMap. Which can do using "ds_map_delete( map , key )" like below:

ds_map_delete( MyMap , "Server IP" );

Liek nay data structure, ds_maps use dynamic memory thus we need to delete our ds_map when we no longer have need for it, this can be done using "ds_map_destroy( map )" like this:

ds_map_destroy( MyMap );

There is a lot more information on ds_maps, such as more functions for them in the Game Maker help file, so make sure to check that out. This concludes basic use and information on ds_maps.

————————————————-End Of Tutorial———————————————–

Leave a Comment

Your email address will not be published. Required fields are marked *