Professional Documents
Culture Documents
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-ToClient 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 portforward 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.
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
--- 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 64198 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 ) );
--- 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 client, 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:
--- 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 buffers 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 isnt exaclty dependant upon having a byte alginment higher than 1. In turn this
means you dont need to learn about byte alignment unless youre 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 );
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-----------------------------------------------