Gloomy UDP Networking Beta

Index


Getting Started
How To Get
How To Use
Making First Connection
Make The Connection
Sending Data
Receiving Data
RPC Commands
Final Notes


Code Documentation

Official Discord

Support Me On Patreon

Change Log


GETTING STARTED


Gloomy Networking is a highly efficient UDP networking library that is flexible enough to be deployed to any type of Multiplayer model whether be it Client / Server, Peer 2 Peer, Client / Host. It abstracts away the need for you to write your own Connection handling or data serialization.

While this system handles a lot for you, you will still need to be prepared to build on top of it quite a bit! This project is recommended for all levels and abstracts out many tedious interactions in an efficient way. It is perfect for someone that still wants full control over their games networking architecture, but having a prebuilt and reliable communication layer.

 

What is UDP and why should I care?


Many times you will see GameMaker games implement TCP as its protocol since it is relatively easy to get setup, and while this may be fine for slower paced games like turn based strategy it is not good for realtime multiplayer. TCP is very slow slow since it guarantees all of your packets arrive to the recipient. When you send a packet you must wait until the receiver responds saying that they have read your message before processing your next one. UDP stands User Datagram Protocol, and is the standard for realtime video games that rely on frequent game state update.

Games such as Call of Duty, Overwatch, and Street Fighter are implemented using UDP. The issue with UDP is that there is no connection. You send a packet to an address and do not know if it was received. Because of this you must write your own system to handle connections which can get very messy in GameMaker, and be hundreds of lines of code. The Gloomy Networking Framework handles all of this for you allowing you to establish connections in just a couple lines of code.

 

How do I get it?


Gloomy Networking must be purchased from the GameMaker asset store. This allows you to easily import the file nessasary into your project from within GameMaker using your library. While in beta this asset will only be $4.99, but the price will increase to $9.99 when the beta is over. Thank you for supporting me and allowing me to create assets like this to help developers realize their dream games!

Once you own the asset it will appear in your MarketPlace library, and you can then import it into your projects from inside of GameMaker!

 

How do I use it?


After importing the files into your project you will notice that you now have a script and object folders named Gloomy Networking. You should never have to touch the (Private) sections in the Scripts folder nor any of the Objects within its folder. They are all handled from within the system.

 

Lets Make Our First Connection


GloomyNet uses a concept of “Connections” that allows you to create any type of multiplayer that you would like. Here is how it works…

So lets get into some code. Lets establish our own side of the network. Note that all GloomyNet functions are prefixed with “gnet_”. The functions that you will need to use are in the “Public” folder.

/// Create and initialize our Network. On fail all network related resources are destroyed.
/// Place this in the Create event of a New Object, and place this new object in a room.
var success = gnet_start_network(10, 2321, -1, "Test User");

if (success)
{
    // !! Always use show_message_async when dealing with networking.
    // !! Using regular show_message will block GameMaker from receiving data!
    show_message_async("Network creation success on port " + string(global.gnet_myPort));
}
else
{
    show_message_async("A socket could not be established for your network. Networking failed.")
}

For this we call “gnet_start_network” specifying the maximum amount of connections that we want to allow to exist on our instance of the game, which is 10.

Next we specify a “Protocol ID”, this is basically a signature for your game. Any packet received on your specified port that is not prefixed by this value is ignored. The number can be any random number of your choice between 1 - 65535. This number must not ever change after your game has been released to the public or they will not be able to communicate. You will never have to touch this value again.

The third argument is a specified port. -1 tells the system to search for a free port to use. This port can always be accessed via global.gnet_myPort. For things like servers or hosts, you may want to specify a specific port so that you will be definite about what port connections need to send requests to.

Finally we supply a “username”. This is just what it sounds like, and was put in place to save you from having to implement a packet to perform this later on. You can retreive a connections userName at anytime using the script gnet_get_username(connectionId).

After the script is called we have saved a boolean value into our success variable. This will allow us to determine if we successfully setup our networking or not.

 

Make A Connection


If your socket was successfully created, you are ready to establish a connection! In the SpaceBar Pressed event place the following code.

/// SpaceBar Pressed
/// 127.0.0.1 is the "LocalHost" which is just an address back to your own computer

var portToSendRequest = get_integer("What port would you like to try to connect to?", 3000);

var requestSendResult = gnet_connect("127.0.0.1", portToSendRequest);

// Request send result is an array with 2 values! If the first value is null
// then we were not able to send a connection request and the 2nd value will contain
// a String Message!
if (requestSendResult[0] == null)
{
    show_message_async(requestSendResult[1]); // Displays a message with the reason.
}

This piece of code will prompt the the player to enter a port to attempt to connect to. It then sends a connection packet to your localhost at the specified port. It will attempt this a few times if a connection cannot be established then the Connection is destroyed and gnet_OnDisconnection will be called. This is a script that you can put code in if you need to perform game logic based on it happening.

If a connection was successfully created then gnet_OnNewConnection() will be called on the receiving players machine. In this script a unique connectionId is given to you which you can use identify and communicate with said player. In addition gnet_OnConnectionReply() is available which will provide you with reponse information on the sending machine. This is called and a response type is provided. (Just check out the script)

So lets notify ourself that a connection was made. Go into the ‘gnet_OnConnectionReply()’ script and make it look like below.

/// Existing Script -> GloomyNet FrameWork -> Public -> Callbacks -> gnet_OnConnectionReply()
var _connectionResult = argument0;
var _connectionId     = argument1;

switch (_connectionResult)
{
	case ConnectionAddResult.AlreadyConnected:
		show_message_async("We are already connected to this address");
	break;
	case ConnectionAddResult.MaxPlayer:
		show_message_async("There is no room for us to connect to this address.. :( its full")
	break;
	case ConnectionAddResult.Success:
		show_message_async("We have successfully established a connection! Press F1 to see some stats!");
	break;
}

When this script is called we recieve a connection result which contains one of these three values. This allows you to handle any game logic you need to based on the result.

Be sure to check the output window often. There is extensive logging in this networking system notifying you of many things. If you do not want to see most of the networking info you can go to the

Framework(Private) -> Statics -> gnet_Macros script, and change

#macro LOG_LEVEL LogLevel.Debug to #macro LOG_LEVEL LogLevel.Info.

Now on the receiving end we need to catch the event to notify ourselves that a connection was received! Go to the gnet_OnNewConnection() script and use this code!

var _connectionId = argument0;

show_message_async("We have established a new connection and assigned it a connection id: " + string(_connectionId));

Lets Test It

To test it so far you can either open a new IDE and open the same project or go to Build -> Create Executable -> Package as a zip -> Unzip and run EXE. The first option is best to get debug info in your output window from both sides, while the second option is best if you want to test with more than 2 connections.

 

Lets Send Some Data!


Obviously we need to do more than just make a connection. We need to send some kind of data back and forth! GloomyNet makes this easy while still being very efficient! Let us go over how packets are created and sent.

Packet Layouts

GloomyNet uses the concept of a packet layout to allow you to define the “types” of your data only once, and never have to worry about writing/reading data to and from buffers manually. Types need to be defined so that GameMaker knows how to store data in binary form before sending, and also how to interpret that binary data upon reception. You can define a packet layout in the gnet_packet_layouts() script.

/// GloomyNet Framework -> Public -> Start_Stop -> gnet_packet_layouts()
enum GamePacketType
{
    PositionUpdate,     // Equivalent to the integer 0
    BulletFired,        // Equivalent to the integer 1
    PlayerDied          // Equivalent to the integer 2
} 

// Create a layout type ready to store x and y coordinates of an object!
// We will create the scr_OnReceivePositionUpdate in the next step! Dont worry!
gnet_packet_layout_create(GamePacketType.PositionUpdate, scr_OnReceievePositionUpdate, buffer_f32, buffer_f32);

A packet layout contains three parts. The first is a “PacketId” which is used to identify the type of packet this is. This is just an integer value between 0 - 219. It is a 1 byte value that helps determine what data is expected to follow. It is recommended that you create a custom ENUM for identification. If you are not familiar with ENUMS read up here: Enum Reference. An enum is basically just a way to give an integer a name and make it more readable!

The second part of a packet layout is a script id. This is a script that will be called when the specified packet type is receieved. The connection id of the sender + an array with the data recieved will be passed to this script when received. This is where you handle the data for the given packet type.

The final part of a packet layout is a sequence of data types. You must put some thought into the type of data that is being sent. An overview of the different types can be found here Data Types. Note that you will need to read over the data types section here so that you know what kind of data can be stored. You are aiming for the data type that is sufficient for any range of values a given variable may be, while minimizing the amount of bytes is needed.

 

Little More Explanation If You Need..

For example, if you are sending your players coordinates they are decimal values that can be negative (if you let your players into the -x and -y parts), and may also be very large if you have big maps. Using the type buffer_f32 we can safely store any value between -16777216 and 16777216. The documents state that buffer_f16 is not supported which would take 2 less bytes. The letter stands for “float” and the number stands for how many “bits” will be occupied by the data. 32 “bits” = 4 bytes.

On the other hand if you were wanting to send something like a “clientId” where you are only allowing 20 players to connect at a time, we want to store a POSITIVE number between 0 and 20. In this case we would choose to use buffer_u8. This stands for a 1 byte integer that cannot be negative. This type can store a value between 0 - 255, perfect! The u stands for “unsigned” which means that there is not a bit that represents whether the number is positive or negative. This frees up one of the 8 bits to be used to represent a larger number. If you see buffer_s8 this means a “signed” 1 byte integer. This type can be positive or negative but instead stores values between -128 and 128.

 

Lets use our packet layout…


Okay we have our packet_layout defined so lets use it to create a packet of data. Also note that if you place the packet layout in the gnet_packet_layouts() script it is automatically called when the network initializes.

/// In a keypress event

// Instead of set numbers you would send your X and Y position, 
// but any number will work here for testing.

var _packet = gnet_packet_build(GamePacketType.PositionUpdate, 10.21, 5.32);

// Now we have a Packet built and layed out how we defined earlier!

gnet_packet_send_all(_packet);

All we do here is build a packet, and specify the data to send. We make sure we pass in data in the same order as is defined in our packet layout! Finally we call gnet_packet_send_all(_packet). This sends our packet to all established connections. You can alternatively use gnet_packet_send_to_id(_packet, connectionId) in order to only send the data to a specific connection.

So this is pretty simple. We called to build a packet using the packet layout we defined for GamePacketType.PositionUpdate. There is some basic type checking at runtime so make sure to check your logs. It will tell you if you didnt supply the expected number of arguments, or wrong argument types. It is still possible for you to specify a type of for instance buffer_u8 but pass in a negative number. In this case the data would send fine, but you would not receive the expected value on the other side! You need to be vigilant to pick your data types correctly. It is an essential part of creating effective Multiplayer games!

 

Receiving And Handling Data


So how do we receieve and handle this data? Well, it is pretty simple! The script we specified in our packet layout is scr_OnReceievePositionUpdate, so lets create that script. Inside of any script that is given as part of a packet layout two arguments are given. The connection id of the sender as argument0 and array of the data received as argument1.

/// scr_OnReceievePositionUpdate()
var _connectionId  = argument0;
var _receivedData  = argument1;

var _xReceived = _receivedData[0];
var _yReceived = _receivedData[1];

show_message_async("A Position Update Was Sent From Connection " + string(_connectionId));
show_message_async("Coordinates received are..");
show_message_async("X: " + string(_xReceived) + " Y: " + string(_yReceived));

That is it! You can now send data using the given packet type, and receive/handle the data easily in this script! As long as you are consistent with your data types and values it is very simple! You should now be able to hit a key to send a message, and your other instance of the game will receive it and display these messages!

 

Sending variable amounts of data


Sometimes you may need to send a set of data multiple times in the same packet. For example, if you want to send the positions for EVERY player using 1 packet you may have 5 players or 10 players, how do we define a packet that will handle this? GloomyNet has the ability to serialize arrays using the format below.

/// Creating a packet layout that expects an array of values
gnet_packet_layout_create(GamePacketType.AllPlayerUpdate, 
                          cb_OnAllPlayerUpdate,
                          GNET_ARRAY_COUNT, 
                          [buffer_u8, buffer_f32, buffer_f32, buffer_s16]);

// Usage - Build an array that stores an array of data for each player!
for (var i = 0; i < numPlayers; i++)
{
    // Iterate through each player, storing their data in a player array
    var _player = instance_find(Player, i);

    with (_player)
    {
        _playersArray[i] = [m_Owner.m_ClientId, self.x, self.y, m_Angle];
    }
}

// Build the packet. Notice we pass in the size of the array,
// Then we pass in the array where each index contains an array of data
// Where the data corresponds to the mapping we define upone layout creation
var _buff = gnet_packet_build(GamePacketType.AllPlayerUpdate, numPlayers, _playersArray);

So this is about the most complicated thing we have to do. Lets walk through it. First we created the definition. When storing a variable amount of repeating data we have to prefix the data with GNET_ARRAY_COUNT, which will simply be the array length, but this type specificly is used when the system builds the packet in order to know to expect an array of values coming up.

Next in brackets we specify the data types that are expected in each index of the array. Specifically the given values [buffer_u8, buffer_f32, buffer_f32, buffer_s16] are meant to store a ["clientId", "x", "y", "angle"]. Notice that we store this data in an array index where that storage array can be of any length. When we receive this data we have to access the nexted arrays like something below.

/// scr_OnAllPlayerUpdate()
var _connectionId = argument0;
var _data         = argument1;

// Notice that the array is actually pos 0
var _playerArray = _data[0]; 

// The length of the array is actually not sent across the net
// So here we recalculate.
var _arrSize  = array_length_1d(_playerArray);
var _myClient = ClientManager.m_OurClient;

// Go through each index of the array and retreive values
for (var i = 0; i < _arrSize; i++)
{
    // A single players data
    var _singlePlayerData = _playerArray[i];

    var _clientId = _singlePlayerData[0];
    var _x        = _singlePlayerData[1];
    var _y        = _singlePlayerData[2];
    var _angle    = _singlePlayerData[3];
}

Note that in the packet layout, and build we specified the arrays SIZE. This is not available in the received array. This may change in the future now that I think of it, but for now use array_length_1d in order to get the length of the array

Sending arrays is very powerful and would generally be much more difficult then this if you were writing your own buffers. Take a bit to soak this in, once you get it the first time it will become easy to understand later.

 

Using Rpc Commands


RPC stands for Remote Procedure Call. This allows you to send a command for a certain script to be executed by a different player along with arguments to use. A simple use for this is sending a command for another player to take damage. In GloomyNet you do not need to provide packet layouts for these type of commands. The types are infered automatically, and because of this they are not as efficient as sending packets with packet layouts, so they should be used more sparingly. Below is an example.

The three functions you should be concerned with.

In order to send an RPC command we simply have to call gnet_rpc_send.

// Sends an RPC to a connection id, telling them to call scr_TakeDamage and use 10
// as an argument
gnet_rpc_send(otherPlayerConnectionId, scr_TakeDamage, 10);

After sending, we need to be able to handle that command. Go ahead and open up Public -> Callbacks -> gnet_OnRPCMessage(). Here you will see the default implemenation, and you can leave it at that if you are doing Peer 2 Peer. If you are building Client / Server you may want the server to do something with the data, then send that data to the target clientId (in this case the target clientId would need to be passed in as an argument). Lets just go over a couple things though.

// This default handling will just go ahead and call the specified script with the
// given arguments
var _buffer = argument0; 
var _script = argument1;
var _arguments = argument2;

gnet_rpc_execute(_script, _arguments);

You can see that three arguments are passed in to this Callback. Buffer is the actual packet that was received. You could send this packet to another connection Id if needed. It is the exact same packet that was sent to us by the original sender. Arguments is an array of the arguments we received. You can see that this array is passed into the gnet_rpc_execute script. Finally we have the script, which is simply the scriptId to execute.

Okay now that the default info is detailed lets see what it would look like in the scr_TakeDamage script.

/// scr_TakeDamage

var damage = argument[0];

with (myLocalPlayer)
{
    health -= damage;
}

And that is it! You can now easily call a script on another players computer! You don’t have to setup packet layouts for EVERYTHING, you canse use these for smaller and more infrequent actions!

Final Notes


This framework is still undergoing changes that will improve on the current system. In addition I will be releasing frameworks built on top of this for both Server / Client and Peer 2 Peer games. This will just be another layer on top of this that will be aimed at specific multiplayer models. Look out for my video tutorials so that you may get an even better understanding. Multiplayer is not easy to program, and this framework takes NO shortcuts. It is implemented in a very efficient way that is both scaleable and reliable.

Any bugs found can be reported to the official discord for GloomyNet which can be found here in the GloomyNet UDP Section Channel DISCORD