GMnet Manual¶
Here you can find the manuals for all GMnet products.
Getting Started¶
GMnet ENGINE is a multiplayer engine for Game Maker: Studio by YoYoGames. It works by syncing instance variables to other players and has many other features. If you want to get started and learn how to use GMnet ENGINE, start with the Tutorial.
Note
GMnet ENGINE is bundled with GMnet PUNCH. All functionality is implemented in GMnet ENGINE. You don’t need to read the PUNCH manual. If you want to use GMnet PUNCH with your own multiplayer engine, please continue reading the GMnet PUNCH manual instead.
Tutorial¶
What is GMnet ENGINE?¶
GMnet ENGINE allows you to make multiplayer games in Game Maker: Studio with nearly zero knowledge in networking.
Everything in GMnet ENGINE is done via the key concept of Game Maker: Objects and Instances.
You simply tell the engine what variables of what object should be synced and how. The engine takes care of sending and recieving the data and the whole client and server architecture.
This means your game’s network functions are easy to use and easy to maintain.
So, let’s waste no time, and let’s get started with making your very first game with GMnet ENGINE.
Basic configuration¶
In this tutorial we want to make a simple platformer with some neat little extras to demonstrate what GMnet ENGINE can do. Our platformer will have:
- Walls/Players/Physics (not Box2D) (obviously)
- Multiple rooms
- An overlay that shows connected players
- A night and day cycle
- A basic chat system
Adding the engine to a project¶
Before we can get started, add the asset to an empty project. Depending on what you want to do, you have three options.
- If you want to follow this tutorial yourself, you should add
ALL scripts and sprites and the object
obj_htme
to your project. - If you want to just read through this tutorial and test the demo game, simply add everything.
- When starting completely blank or when you want to add the engine to your own game, add all folders named htme and udphp but not the htme_demo folders.
Configuration¶
Great! No matter what option you chose, you propably want to take a look at the initiation and configuration, so let me walk you through.
Before we look at the configuration we check the initiation script. The init can be found in ``scripts/htme/htme_init``.
The first thing you’ll find under initiation is randomize();
.
That assures that the random operations Game Maker does are truly
random, which is important for the engine. This is good to know when you make your game.
Thats all for the initiation for now. Let’s go to configuration.
The configuration can be found in ``scripts/htme/htme_config``.
First you see debug level. Valid options can be found
in the enum htme_debug
and how debugging works is explained in the
comment. Leave it on the default option for now.
The next thing is gmversionpick. Depending on what version of game maker
you are using you may need to change this.
If you use above 1.4.1567 set this value to 1.
If you use 1.4.1567 or below set this value to 3. If you set it to 3
you need to go to htme_serverStart
and comment this line:
switch (gmversionpick)
{
// You maybe dont got network_create_socket_ext just add // in front of it
//case 1: self.socketOrServer = network_create_socket_ext(network_socket_udp,port); break;
case 2: self.socketOrServer = network_create_socket(network_socket_udp); break;
case 3: self.socketOrServer = network_create_server(network_socket_udp,port,maxclients); break;
default: htme_error_message_handler("Go to script: htme_serverStart and decomment the one you use!");
}
This is because if you use 1.4.1567 or below you dont got the network_create_socket_ext function.
Let’s skip ahead to ``self.global_timeout``. That’s actually the
only important configuration option and even that can stay on default if
you want. Timeout specifies after how many steps of inactivity the
connection between client and server will die and you will be
disconnected. The default is 5*room_speed
which basicly means 5
seconds. When client and server don’t communicate to each other for 5
seconds, they are considered to be disconnected.
So yeah that was all there is to configure for now.
Follow the next steps of the configuration. They will explain how GMnet ENGINE (or GMnet PUNCH to be precise) enables you to connect to other players even behind a firewall.
Setup GMnet PUNCH¶
Note
This topic requires GMnet PUNCH enabled. More information below.
If you want GMnet ENGINE can be used to connect players even if the do not forward ports in their firewall.
To understand this, you need to know, that whenever you want to play a game with someone there are three options:
- Dedicated servers to play on, like most shooters have
- Forwarding a port on the servers firewall, most common when playing strategy games or hosting an own dedicated server of some game
- “Hole-Punching” - GMnet PUNCH, which ships with GMnet ENGINE version, allows UDP Hole Punching. This technique allows two players to connect to each other even when no ports are forwarded at all. This works most of the time, however not always. Especially in mobile networks or buisness networks that might fail. Hole Punching is often used by many games where multiplayer should be easy to do by the user.
That means if you disable GMnet PUNCH, your players either can’t play together online, only locally, or the server has to open ports.
GMnet PUNCH configuration¶
In htme_config
there are several variables you need to change to use
GMnet PUNCH.
self.use_udphp
: This is the most obvious one. Set this to
true, to enable GMnet PUNCH.
self.udphp_master_ip
and self.udphp_master_port
: You
need to set these to the ip and the port of your mediation server. What
a mediation server is, is explained in the next section. You basically
need to host a server, to which server and clients connect to share
their information.
self.udphp_rctintv
: How often (in steps) the server should
reconnect to the mediation server to make sure it is still connected. It
is important that the server is connected to the mediation server, so
don’t set this too high. The default option should be fine.
If you don’t have a server yet you can use the ip 95.85.63.183 and the port 6510 for testing only.
Hosting a master server¶
Note
This topic requires GMnet PUNCH enabled.
Note
GMnet GATE PUNCH is outdated and will be replaced by a new master server in the next version.
To use the magic of GMnet PUNCH you need to host a mediation/master/rendevouz server. The server is a small application written in Java that needs to be run on a server with a forwarded/open port.
To explain why you need this, let me explain how the hole punching works:
- The server connects with the master server (GMnet GATE.PUNCH) and its port and ip get registered.
- When the clients want to connect to this server, his port and ip also get registered and sent to the server. And the client gets the ip and port of the server.
- Now both send UDP packets to each other at the same time: The hole is punched.
We need the master server (GMnet GATE.PUNCH) because we need to make the server aware of the fact that a client wants to connect.
The download can be found here:
http://gmnet.parakoopa.de/gatepunch
The server is very basic, feel free to adjust it to your needs.
Currently the server binary listens on port 6510 and there to change it if you don’t want to compile the server yourself, but we will add that option in the future.
To start it, run: java -jar gmnet_gate_punch.jar
from the folder you
saved the server in. Under Windows you might need to replace java
with the full installation path of java. Under linux I recommend
screen
to run it in the background on a server.
Hosting¶
For cheap hosting I personally recommend digitalocean. Server start at 0.0015 per hour (5 per month) and you can cancel at every time.
Google yourself a promo code and you might get two months for free. There are also tutorial on how to set up java on these servers. They are basically linux servers with complete access.
If you want to use digitalocean, please use this code to register: https://www.digitalocean.com/?refcode=1bff6d6dff1a
Some tutorials¶
If you want to know how to login to the server: How To Create Your First DigitalOcean Droplet Virtual Server
For some basic commands and stuff: An Introduction to Linux Basics
If you want to install java: How To Install Java on Ubuntu with Apt-Get
For transfering the master server: How To Use Filezilla to Transfer and Manage Files Securely on your VPS
[You can skip the stuff with the keyfile and everything. Simply install filezilla and connect to sftp://yourip with your username and password]
When you are done simply execute java -jar gmnet_gate_punch.jar
As I said, I recommend install screen, it allows you to run commands even if you are not logged in, running in the background and you can just switch to them at any time: How to Install and Use Screen on an Ubuntu Cloud Server
You can also create an autostart service for the server and run it on startup.
Starting the engine¶
Great! Now that we are set up, let’s actually make a game.
Before we start, if you want to follow the tutorial make sure again,
that you added all sprites, all scripts and the object obj_htme
to
an empty project.
Connection done!¶
Theoretically, we could now play the game. Of course, there is no game.
The game will crash if we try to start a server because our game room
htme_rom_demo
does not exist.
For now, create an empty room just like you did before and call it
htme_rom_demo
.
Now, when you fire up your game twice, you should be able to start a server on one game, which should lead you to the empty room.
On the other game you should be able to connect to this server by connecting to the ip 127.0.0.1. If it was successfull, the “Connecting...” message should disappear, and you’ll find yourself in the same empty room as the server. Yay!
Setting up the basic platformer¶
We now need to create a simple platformer. Since this is not supposed to be a tutorial on platformers, and the platformer we are going to use is simple and pretty bad, simply follow the instructions and copy the code.
1. Setup our game room¶
Give the game room htme_rom_demo
we created a nice background
color and the dimensions 800x600. For this demo, we are not going
to use views.
2. Create a wall¶
Create a solid object, called htme_obj_wall
. Give it the sprite
htme_spr_wall
. This is our new wall. Place it into the game room,
build some walls, a floor and maybe some platforms.
3. Create the player object¶
Next we are going to create our player.
Create the object htme_obj_player
and give it the sprite
htme_spr_player
.
Into the create-Event put the following code:
///Setup basic stuff for the demo platformer.
self.pressed_jump = false;
self.pressed_left = false;
self.pressed_right = false;
self.name = "";
/** Totro generates random names and is not part of the main engine, it's
* another marketplace asset by me :)
*/
var ttr = totro(5,7,1);
self.name = ttr[0];
/** Gives this player a random color. */
self.image_blend = irandom(16777215);
}
The self.pressed...
variables will store if our player pressed the
buttons to move or to jump. We need to store this in a variable as a
preperation for later, when we want to add the player to the engine. But
you’ll see :).
Into the step event put the following code:
///Perform platforming!
//This is a very basic example of a platformer. You should not program your physics like this!
if (place_free(x, y+1)) {
gravity = 0.2;
} else {
gravity = 0;
vspeed = 0;
}
self.pressed_jump = keyboard_check(vk_space);
self.pressed_left = keyboard_check(vk_left);
self.pressed_right = keyboard_check(vk_right);
if (self.pressed_jump && (!vspeed)) {
vspeed = -12
image_xscale = 1;
image_angle = 90;
}
if (self.pressed_right) {
x+=10;
image_xscale = 1;
image_angle = 0;
}
if (self.pressed_left) {
x-=10;
image_xscale = -1;
image_angle = 0;
}
if (vspeed < 0) vspeed=vspeed+gravity;
As you can see, we currently just put the button presses into the
variables self.pressed...
and check them. We will change that later.
Now create a Collision with htme_obj_wall event and put the following code in it:
///Set speed to 0 when hitting a top wall
if (!place_free(x, y-16)) {
move_contact_solid(90,-1);
vspeed = 0;
}
if (!place_free(x, y+16)) {
move_contact_solid(270,-1);
gravity = 0;
vspeed = 0;
}
Into the draw event, we will put some code to draw the name we randomly generated in the create event. First put “draw self“” into the event and then this code:
///Draw nameplate
draw_set_color(image_blend);
draw_set_halign(fa_center);
draw_text(x,y-sprite_yoffset-5-string_height(self.name),self.name);
draw_set_halign(fa_left);
draw_set_color(c_white);
Now, the last thing to do is to place one htme_obj_player
in the play room htme_rom_demo
.
Every player that joins the game will create its own player object.
If you add two player objects in the room. Every player create two player objects.
Basic platformer: Done!
4. Test¶
Start the game and then start the server. You can now test out how bad the platformer really is. But hey, it does the job. The next tutorial is actually related to the engine again.
The network controller¶
Before we continue, let’s create an object that makes sure, the engine is still running.
The engine will stop running on the client-side, when the connection between server and client is lost.
Create the object htme_obj_network_control
. Into the
Step-Event put the following code:
///Check if engine running
if (htme_isLostConnection()) {
htme_error_message_handler("Game Server or Client died! Go back to menu...");
// Clean other persistent non synced objects from room
with htme_obj_chat instance_destroy();
with htme_obj_playerlist instance_destroy();
room_goto(htme_rom_menu);
}
If htme_isLostConnection()
returns true at any point after your client
connected to the server, you can be very certain, that you lost
conection to the server. Into the if Statement, simply put your own
code to handle this scenario. htme_isLostConnection()
should always return
false for the server, unless the server dies due to an errror.
Oh, and don’t forget to put the object into the htme_rom_demo
.
Adding a player¶
Okay, okay, I know we already added a player in Step 5.
The create event¶
To set the player up, we only have to make minimal adjustments. Create a new code block in the create event, and insert it before the other block.
Start the code block with
mp_sync();
This will tell the engine to sync this object. Once this was created on one client, this instance wil also be sent to all other players.
If you add one player instance to your room and connect 4 players togther, each player will have four player instances.
Now we need to tell the engine what variables to sync and how:
mp_addPosition("Pos",5*room_speed);
This will tell the engine to sync the position variables x
and y
every 5 seconds. 5*room_speed
means 5 seconds. The first argument of
mp_addPosition
is the name of group. You can choose that however you
want.
What we just added is a so called variable group. The group is
called “Pos”, get’s synced every 5 seconds and sync the variables x
and y
.
Let’s set how the position should be synced:
mp_setType("Pos",mp_type.SMART);
This changes the sync type to the variable group “Pos”. The default sync type is FAST. We are changing it to SMART. Here is a list of what every sync type does:
- FAST (default, you don’t have to use
mp_setType
if you want to use that): In our case, if we had chosen FAST, the engine would send the position of our player every 5 seoncds once. Since we are using UDP for networking, there is no assurance, that it will actually arrive. That means we don’t know if the other players actually get our position every 5 seoncds. The packet could get lost. However, using FAST is very... FAST. You will see when we want to use FAST later in this section. - IMPORTANT: When using IMPORTANT, in our case the position would still be synced every 5 seoncds. However, we are using a special way in GMnet ENGINE to make sure the packet arrives. Every 5 seconds, the server/client will flood the other players with the information, until they respond back, that they got it. That’s how we assure the information is actually sent. This is however a pretty traffic heavy operation and using that in too short intervals will cause lagg in both connection and framerate.
- SMART: This is like IMPORTANT but better. Instead of syncing every 5 seconds, we will only sync every 5 seconds what has changed. If only the x position changed, we are not syncing the y position. If our position has not changed at all, we are not syncing. This is basicly a more traffic-friendly version of IMPORTANT.
We are using SMART here and relatively large intervals, because we only sync the position as a backup. As you will see later, we are going to sync the button inputs every single step. We only sync the position to make sure, the players don’t get desynchronized.
The** button input later will be synced FAST**. That means **some packets could be lost**, and after some time the player could be on two completly different parts of the level, depending on the complexity of your platformer. To make sure that doesn’t happen, **we reset the position every 5 seconds**.
Now, the thing is, if we reset it every 5 seconds, it might happen that the players are just some pixels off. In that case our players might slightly “flicker” whenever the position resets. That doesn’t look nice.
Let’s add a little tolerance range. If the position at is less than 20 pixels away from where it should be, the other players should not apply this change locally, so it doesn’t flicker that much:
mp_tolerance("Pos",20);
Great. Position is set up. Now, just to make sure, let’s also sync the basic Game Maker physics and drawing variables:
/**
* Tell the engine to add the basic drawing variables:
* image_alpha,image_angle,image_blend,image_index,image_speed,image_xscale
* image_yscale,visible
*/
mp_addBuiltinBasic("basicDrawing",15*room_speed);
mp_setType("basicDrawing",mp_type.SMART);
/**
* Tell the engine to add the builtin GameMaker variables:
* direction,gravity,gravity_direction,friction,hspeed,vspeed
*/
mp_addBuiltinPhysics("basicPhysics",15*room_speed);
mp_setType("basicPhysics",mp_type.SMART);
We don’t need to sync them that frequently. If we sync physics to
frequently that might look weird and we are only syncing the basic
Drawing stuff for the player color (image_blend
) which doesn’t
change anyway, and the image_xscale
and iamge_angle
which
controls how the player faces, which isn’t that critical.
Now we also want to sync the name:
mp_add("playerName","name",buffer_string,60*room_speed);
mp_setType("playerName",mp_type.SMART);
This is using mp_add
to sync our own variables. The syntax is
slightly more compelx and we need to do some things to make this work
later, but let’s just see what we got here: * The first argument is
again the name of the group * In the second argument you specify the
names of the variables you want to sync, seperated by commas. We
stored the player name in the “name” variable. * The third argument is
the buffer type. You can find information on which buffer type you
need to use on this manual page. All variables
need to have the same buffer type. buffer_string
simply means that
the variable “name” stores a string. * The last argument is the syncing
interval, just as before.
We decide for a 60 seconds interval, because the name will never change. We could have also used 20 years, that wouldn’t make a difference. We only need to make sure the engine syncs it on login and some other critical events, and it does that automatically, no matter what interval we choose. We still need to make it SMART because we need to make sure it REALLY arrived at those events.
Next up are the controls. Remember how we stored the button input in seperate variables? Well now you might know why:
mp_add("controls","pressed_jump,pressed_left,pressed_right",buffer_bool,1);
The “buffer type” is buffer_bool
, because our “pressed_” variables
are booleans. 1/0, true/false.
This will sync the button input to all players every single frame no matter what. We don’t want to have it SMART or IMPORTANT. FAST is the way to go, since there is no point in checking if the data arrives, because we are syncing the button input every step anyway.
Some things needed when using mp_add
¶
mp_add
to sync our own variables, we need to
make some code changes. We need to send the variables to the engine at
the beginning of the step, and retrieve the data at the end.mp_addPosition
,
mp_addBuiltinBasic
and mp_addBuiltinPhysics
, because we
hardcoded them.If you don’t know what any of that means what I just said, don’t worry. It’s complicated.
The only things you need to know, we will explain them now:
For every object where you use mp_add
add the following to the
begin step event:
mp_map_syncIn("name",self.name);
mp_map_syncIn("pressed_jump",self.pressed_jump);
mp_map_syncIn("pressed_left",self.pressed_left);
mp_map_syncIn("pressed_right",self.pressed_right);
Replace the names with the names of your synced variables. These are the variables that we created above for our tutorial player.
All changes to these variables need to be made BEFORE using these
functions. That means you either have to change them in begin step, or
call mp_map_syncIn
again after you changed variables. We recommend
the first. And that’s also what we are going to do in a minute.
In the end step event add the following code to retrieve the variables again:
self.name = mp_map_syncOut("name", self.name);
self.pressed_jump = mp_map_syncOut("pressed_jump", self.pressed_jump);
self.pressed_left = mp_map_syncOut("pressed_left", self.pressed_left);
self.pressed_right = mp_map_syncOut("pressed_right", self.pressed_right);
Setting up controls for synchonization¶
Last thing we need to do, is to move this code out of the step event we created earlier:
self.pressed_jump = keyboard_check(vk_space);
self.pressed_left = keyboard_check(vk_left);
self.pressed_right = keyboard_check(vk_right);
Simply remove it. In begin step, add the following code before the other code:
if (htme_isLocal()) {
self.pressed_jump = keyboard_check(vk_space);
self.pressed_left = keyboard_check(vk_left);
self.pressed_right = keyboard_check(vk_right);
}
It should now look like this:
if (htme_isLocal()) {
self.pressed_jump = keyboard_check(vk_space);
self.pressed_left = keyboard_check(vk_left);
self.pressed_right = keyboard_check(vk_right);
}
mp_map_syncIn("name",self.name);
mp_map_syncIn("pressed_jump",self.pressed_jump);
mp_map_syncIn("pressed_left",self.pressed_left);
mp_map_syncIn("pressed_right",self.pressed_right);
All the code we just pasted in begin step does, is check if this is the instance that was locally created and then writes the button input of the players into the variables.
This way when we have 4 players, we only move the instance we control,
the instance we locally created. The self.pressed_jump...
variables
will be changed by the other 3 players for the rest of the three
instances.
Look at the following table from the view of player 1:
Instance/Player | Controlled via buttons | Controlled via engine |
---|---|---|
Ours / Player 1 | Yes | No |
Player 2’ s | No | Yes, buttons from Player 2 |
Player 3’ s | No | Yes, buttons from Player 3 |
Player 4’ s | No | Yes, buttons from Player 4 |
Test¶
Fire up two games and create a server / connect to 127.0.0.1.
You should now see both players, and should see that we now have a multiplayer platformer.
A second room and doors¶
Now you know nearly everything needed to make a very simple multiplayer game.
The next sections will cover the following more advanced, but still not very difficult, topics:
- Using the multiplayer engine with mutliple rooms
- Working with the data of the multiplayer engine
We are now going to create a second room. Simply** copy**
htme_rom_demo
and restructure the walls a bit. Maybe you could also
change the background color. Remove all other objects from the room,
except the walls and ``htme_obj_network_controller``.
There are two aproaches you could use if you want the player to enter this room:
- Place a new player instance in
htme_rom_demo2
, make the player object NOT persistent - Make the player instance persistent; Persistent objects in Game Maker exist even after you change the room.
For this tutorial we are using the second approach. The reason for that, is that we also want to teach you, how persistent objects work with the instance. But if you solve your game using the first method, this should also work in most cases. Please note that the second approach is recommend though.
Make your ``htme_obj_player`` persistent. That’s all we need to do in the player object if we want him to move to a second room.
Now create the object htme_obj_door
with the
spritehtme_spr_door
.
Create a Collsion with ``htme_obj_player`` event and add this code:
///CHECK IF DOOR ENTERED
var isLocal;
with (other) {isLocal = htme_isLocal();}
if (isLocal && keyboard_check_pressed(vk_up)) {
var dest = htme_rom_demo2;
if (room == htme_rom_demo2) dest = htme_rom_demo;
room_goto(dest);
}
This checks when a player is standing in front of a door if the other
instance, which is an instance of the object htme_obj_player
is our
local player by storing this information in the local variable
isLocal
. When it’s our local player (“we”) we change the room. The
room is changed to htme_rom_demo2
when in htme_rom_demo
and
viceversa. Place a door in each room.
We are done.
Now start a client and server again and try switching rooms. Everything should work as you would expect it. If not, check again if you made the player object persistent and make sure the doors are on the same position in both rooms.
Some technical stuff¶
When you make persistent objects, they are only persistent locally.
That means when another player switches room, they will cease to exist in your game. When you enter the room they will be resent and recreated to your client.
Here is a nice little table FOR AN INSTANCE CREATED BY A IN ROOM 1:
We will extend this table later with a third scenrio that makes the instances visible/existent at all time for all players.
Overlay that shows the name of connected players¶
We will now create a list that is displayed in the top left corner of the screen and will look something like this:
Create a new object htme_obj_playerlist
with no sprite, make it
persistent so it exists in both game rooms, and place it in
htme_rom_demo
.
This new object is actually not going to be added to the engine. This
object will simply look for all existing htme_obj_player
instances
and write their metadata on the screen.
draw_set_color(c_white);
draw_text(40,35,"Players:");
Okay, cool, cool. But how on earth do we get all player instances? There are two possibilites:
- You could use
with htme_obj_player {/*Code*/}
to loop through all player instances - The engine provides some tools to get a list of all connected players and a function to get an instance that is controlled by a particular player.
In our case the first solution is the easiest. However we want to teach you what you can do with the engine, and so we are going to use the second method. There might be cases where looping through all instances of an object would be simply to much, for example if you have a race game with 2 players and 6 cpus and you only want to get data of the connected players.
var playerlist = htme_getPlayers();
This will give you a ds_list
containing the hashes of all players.
Each player is identifed by a random 8 character hash.
To loop over this list, add the following for-loop:
for(var i = 0;i<ds_list_size(playerlist);i++) {
var player = ds_list_find_value(playerlist,i);
}
player
will now be the playerhash of a player. We use this hash to
get a player instance of this player. Inside the for loop after
``var player =...`` insert the following:
var instance = htme_findPlayerInstance(htme_obj_player,player);
This will either return an instance or -1. It will return -1 when the player is in another room, because no instance for this player exists. And that’s a problem.
Because that means we can’t actually get the name of the player to display it then. That’s a limitation because we store the player name with our player object. After this tutorial is over you should be able to solve this issue, and I challenge you to do so!
Because of this limitation we will simply show “(other room)” in a grey color, when the player instance does not exist:
if (instance != -1) {
var name = (instance).name;
var _image_blend = (instance).image_blend;
} else {
var name = "(Other room)";
var _image_blend = c_gray;
}
``name`` now contains the name of the current player and ``_image_blend`` the color.
The following code will draw the sprite and the name:
//Draw small player icon
draw_sprite_ext(htme_spr_player,0,50,(i)*20+70,0.5,0.5,0,_image_blend,1);
//Draw player name
draw_set_colour(_image_blend);
draw_text(70,(i)*20+62,name);
Remember, all of this must go into the for-loop.
Start the game and test it out, our player list should work just fine.
Adding day and night¶
Now we want to add time and a day and night cycle. Obviously we want to sync the time between all players.
So let’s just create a new object, I guess?
...But wait. If we simply sync a time object using the engine, we will end up having one time object per player. For 4 players that means we would have four time objects fighting over each other.
Of course we could tell all objects to only increase the time when it’s locally, but then they would all have their own time counter and that would have the same effect of not syncing them at all.
Oh, if there would only be an easy way to fix this... Oh! Wait! There is!
You can check if the object was created on the server with
htme_isServer()
. This way you can** add objects that get
controlled by the server and synced to each player**. You have 1
instance on 4 PCs, instead of 4 instances at 4 PCs.
Let me show you how this works.
Create a new object called htme_obj_time
. Set depth to 1 so it will draw in the background behind the player object. And add the following code
in the beginning of the create event:
if (!htme_isServer()) {
instance_destroy();
exit;
}
Genius!
After that bit of code we add the code to add our new object to the engine, which is of course never executed when created clientside:
mp_sync();
mp_add("time","time",buffer_u16,20*room_speed);
mp_setType("time",mp_type.SMART);
//And at the end we set the time to 1000 - this will be noon
self.time = 1000;
This way the** clients recieve the time every 20 seconds**. We are going to program the counter so, that it counts up, even clientside. That means **each player’s game counts up on their own**, and the server **corrects the time if neccessary**.
Because we used ``mp_add`` we need to add the following code to begin step:
mp_map_syncIn("time",self.time);
And this to end step
self.time = mp_map_syncOut("time", self.time);
The two things left are counting the time up and actually doing something with it.
Increasing the time¶
Remember that, because we use mp_add
, all changes to our time
variable need to be made before mp_map_syncIn
is called. Because of
that, create a new code block in position 1 of begin step:
///Increase the time. If time is greater than 2000, set to zero.
self.time++;
if (self.time > 2000) self.time = 0;
This will count up our time to 2000 which is midnight, and then reset it to 0, which is also midnight. 1000 is noon.
If you want to perform actions on these server controlled instances only
by the server use if (htme_isLocal()) {}
. Everything inside this
statement will only be executed by the server.
Day and night¶
Paste the following code into the Draw-Event. This will change the background color in the first room depending on time and display the time. Room 2 will be interior.
///Draw background
if (room == htme_rom_demo) {
//Draw night/day
//This is not a good way of doing it, but I'm not in the mood for that :D
if (self.time == 0) {
var bgcolor = make_colour_hsv(170,185,0);
} else if (self.time <= 1000) {
var bgcolor = make_colour_hsv(170,185,255/100*(self.time)/10);
} else if (self.time == 1001) {
var bgcolor = make_colour_hsv(170,185,255);
} else if (self.time <= 2000) {
var bgcolor = make_colour_hsv(170,185,255/100*(1000-self.time)/10);
}
draw_set_colour(bgcolor);
draw_rectangle(0,0,room_width,room_height,false);
//Draw time as debug on screen
draw_set_colour(c_white);
draw_text(room_height-70,70,"Time: "+string(self.time));
} else {
draw_set_colour(c_maroon);
draw_rectangle(0,0,room_width,room_height,false);
}
draw_set_colour(c_white);
That’s not right...¶
Now when testing what we just did, you might realize that it doesn’t
work right. Even if you set htme_obj_time
to be
persistent, the time object just vanishes sometimes. Let’s take
a look again at our nice table again:
As you can see in this table, if the server is A and A is in Room 2, the time object will simply disappear on all clients. Or if the server is in Room 1 and the client(s) in Room 2. And if you don’t even set it to be persistent, it even disappears for the server if he is in Room 2. A nightmare!
That’s not good! We want our new time object to allways exist, no matter what!
So, first, set ``htme_obj_time`` to be persistent. Now,** after
the mp_sync();
in the create event** add this:
mp_stayAlive();
This tells the engine to keep this instance alive, no matter what. This ONLY works if the object is persistent!
When we add this to our table, things look like this:
As you can see, with stayAlive enabled, the instance always exists.
Done!¶
Time and day is done. Test it out!
We are now ready for the final chapter...: A chat system...
Chat¶
In this chapter you will learn a new way of syncing information between players. This method was designed for chat messages and similiar things.
For this we use the so called CHAT (Custom Handler for Advanced Traffic) Interface. You will learn how to use it in this chapter and can find more advanced usages of it in chapter Bonus 5 - RPC.
First, let’s create an object ``htme_obj_chat``.
We are storing messages in the variable message. Or at least the last message sent by a player. Then we are syncing it via the engine, and the local instance of that object will loop through all chat instances, like we learned when creating the chat overlay. It will then look for new messages in our “inboxes”, and add them to a list that contains all messages.
Adding a CHAT handler¶
Create a Create-Event and add the following code:
mp_syncAsChatHandler("Chat");
//Setup variables
self.output = ds_list_create();
`mp_syncAsChatHandler <functions/chat/mp_syncAsChatHandler>`__. This function registers the object to be a sender and reciever for string messages for the chanel Chat. `mp_sync <functions/chat/mp_sync>`__ is not needed here. We won’t be syncing the object itself, only messages. But you’ll see what that means in a second.
The ds_list ouput is used to store all chat messages.
Sending a message.¶
We are going to send a message when the player presses the C-key. Create a press C-key Event:
self.str_id = get_string_async("Send a chat message:","");
This script uses ``get_string_async`` to ask the player for the chat message. If you never used that before, things might be a bit complicated, so let me explain:
get_string_async
displays an input box where the player can input
text without locking the game, like get_string
would do. That’s
important, because the client or server will loose connection, if the
game is locked.get_string_async
returns a
number, that we need to store. Inside the Dialog Event, which can be
found under Asynchronous we are retrieving the message:///Process the message the player typed in
var i_d = ds_map_find_value(async_load, "id");
if (i_d == self.str_id) {
if (ds_map_find_value(async_load, "status")) {
if (ds_map_find_value(async_load, "result") != "") {
var message = ds_map_find_value(async_load, "result");
//Send the message using the CHAT Interface.
mp_chatSend(message);
}
}
}
This may seem a bit complicated, the code checks that the message entered was valid, but the important code is the code in the most inner if-clause.
var message = ds_map_find_value(async_load, "result");
retrieves the
message and writes it to the local variable message.
Using `mp_chatSend(message) <functions/chat/mp_chatSend>`__ we are now sending this message to all players. It will also be sent to ourself.
Recieving messages¶
Messages are stored in a ds_queue, which is filled when new messages arrived. We will be checking every step if new messages arrived.
In the step event, add the following code:
if htme_isStarted() {
var queue = mp_chatGetQueue();
while (ds_queue_size(queue) > 0) {
var raw_message = ds_queue_dequeue(queue);
The first line uses
mp_chatGetQueue to get the queue
of messages and then we loop through it until it is empty.
ds_queue_dequeue
will get the oldest message in the queue
and remove it from the queue.
This message however is “encoded”, so we now use two functions to decode it:
var sender = htme_chatGetSender(raw_message);
var message = htme_chatGetMessage(raw_message);
htme_chatGetSender and htme_chatGetMessage will, as their name might suggest, return the author of the message (the hash of the player) and the sended message.
var player_instance = htme_findPlayerInstance(htme_obj_player,sender);
if (player_instance != -1) {
var name = (player_instance).name;
} else {
var name = "(Someboy in another room)";
}
This is the same as in the chapter 9, so check this page of the tutorial for more information.
The last thing we do, is we add the message we recieved to the list we created in the create event, together with who sent it, seperated by a ”:”.
//Add to list of chat output
ds_list_add(self.output,name+": "+message);
}
}
Display the chat.¶
To display the chat, put the following code into the draw-event:
//Get an offset, so we draw the newest line on bottom
var size = ds_list_size(self.output);
var bottomLine = room_height-50;
var offset = size*20;
for(var i = 0;i<size;i++) {
var line = ds_list_find_value(self.output,i);
draw_text(50,bottomLine-offset+i*20,line);
}
That draw event is not essential part of the engine, so I’m not explaining it here. It basically loops over the output list and displays each line.
We are done. Have fun testing it out!
Private messages¶
You can also send private messages to certain players. mp_chatSend supports a second argument, where you can use the hash of a player to only sent your messages to specific players. Try it out!
Conclusion / What’s next¶
You now learned everything there is to know.
Multiplayer with GMnet ENGINE might be a little bit different that what you are used to, but once you got the hang of it, the possibilites are endless.
Play around with the engine for a bit, and eventually you will master it!
If you have any questions, find bugs or have feature suggestions, follow the instructions in the Support section.
If you want to learn even more advanced trics, like sending your own custom packets, check out the “Extending the Engine / Advanced Use” section.
Have fun creating awesome multiplayer games! And if you do, don’t forget to tell us, we really want to see, what you can come up with.
PS: If you are stuck with our challenge here’s a solution (encrypted with Caesar algorithm, shifted 1 letter in the alphabet up):
Dsfbuf b ofx pckfdu uibu tupsft uif obnf pg uif qmbzfst, jotufbe pg iunf_pck_qmbzfs. Nblf ju qfstjtufou boe tubzBmjwf, tp ju bmxbzt fyjtut. Jo uif qmbzfsmjtu boe uif dibu, mppq pwfs ju jotufbe. Bmufsobujwfmz zpv dbo bmtp tupsf uif obnf pg uif qmbzfst jo uif dibu pckfdut.
Bonus: An ONLINE lobby¶
Note
This topic requires GMnet PUNCH enabled.
*NOTE: Version 1.2.0 introduced LAN lobbys. This chapter only covers the online lobby*
The update to version 1.0 introduces a new lobby system for online servers.
That means you can now add an online lobby to your game.
This part of the tutorial will explain everything you’ll need to know. Before we start, please create a new project and load the sample project (add all files from the GMnet ENGINE asset).
We now have everything we created in this tutorial and more. The lobby is one of these things. Let me take you on a tour on how it works.
GMnet PUNCH needs to be activated. Please follow chapter 2 and 3 for this.
Testing the lobby¶
When you press L when starting the game you will be brought to the demo lobby room. This is a very simple demo room that can only show four servers. Once we are done, you’ll be able to create an even better lobby.
To test it, see if a server is online and connect to it via the lobby. Please note that you have to set up a master server before, as explained in chapter 3. If you use the demo master server, please note that there might be “GMnet PUNCH Demo Servers” in the server list. You can not join them, they were created using the UDPHP demo project. Both use the same lobby and the same demo master server.
You can also create a server and fire up a second game and see if you can join it.
The new concepts: Gamenames¶
In htme_config
there is a new variable you need to change when
creating your own game (keep the default for the demo project though,
otherwise you can’t join demo servers via the lobby):
/**
* Shortname of this game
* + version
* Example: htme_demo100
**/
self.gamename = "htme_demo100"
This string represents your game and the version of your game. If you update your game and it’s server is not compatible with older versions, change this string and you can prevent players from joining older servers in the lobby (Please note that this is only for the lobby, if your players are able to connect manually, you need to implement your own way of kicking him out of the game again after he has connected, see How can my clients get the gamename and data strings after they connected? below for more info)
You can change it at any time, for example if you have multiple
gamemodes you want to mark in the lobby via htme_setGamename(name)
and get it via htme_getGamename()
.
Data strings¶
Asside from the gamename string, there are 7 more strings that can be used to identify your game in the lobby.
Start the demo game and create a server. You will be asked to enter the name of the server and a description. These are data strings 2 and 3. The demo lobby uses them for these purposes, but when making your own game, you can use them however you want. 4-8 are unused by the demo project.
Let’s open the N-key event of htme_obj_menu (this event creates the server).
You’ll find this part after the server has successfully been created:
htme_setData(2,get_string("How should this server be called (for the lobby)?","GMnet ENGINE Demo Server"));
htme_setData(3,get_string("Enter a server description (for the lobby)?","A server created for the GMnet ENGINE demo project"));
As you can see, we set the server name and description right after the
server was started using htme_setData(n,string)
. We use 2 for server
name and 3 for description. You can also on the server end retrieve
these strings using htme_getData(n)
.
If you want to update any data string later, after the server connected
to the master server, you need to use htme_commitData()
. This syncs
all data strings to the master server. You MUST NOT use it here, because
the server hasn’t connected to the master server yet!
A note for GMnet PUNCH users: The gamename is data string 1.
Building the lobby¶
Ah, the lobby. Finally we are ready to build it.
The room of the lobby is the room udphhtme_lobby
. This room only
contains the object obj_udphphtme_lobby
. This object controls the
lobby. Let’s dive into it!
The create event¶
//IF YOU USE GMnet PUNCH - it will only let you connect to GMnet PUNCH servers:
if (!script_exists(asset_get_index("htme_init"))) {
self.game = "udphp_demo120"
}
//IF YOU USE GMnet ENGINE - it will only let you conect to GMnet ENGINE servers
else {
self.game = "htme_demo121"
}
//IF YOU USE YOUR OWN SERVER - Change self.game!
///Recieve lobby data from the master server
udphp_downloadServerList(4,"date","DESC",self.game);
First the variable game
gets set. We use this to prevent GMnet
ENGINE players from joining GMnet PUNCH servers and vice-versa. As said
before, this object is used in GMnet ENGINE demo project and GMnet PUNCH
standalone demo project, this is why it has seperate code for both.
The important part here is
[udphp_downloadServerList](functions/udphp_downloadServerList)
. This
will tell GMnet PUNCH (the part of the engine that controls
communication with the master server) to download a list of servers from
the master server. The paramters allow for filtering and sorting the
result, we use this to only get results that match our game name. More
information about what you can filter, can be found on the usage page
of udphp_downloadServerList.
You use your own filtering variables later when creating your lobby.
The networking event¶
///Waits for master server response
udphp_downloadNetworking();
This code checks if the master server sent the server list and updates
it. This is not included in htme_networking();
and therefor has to
be run here.
The draw event¶
The draw event is split up into different sub-scripts:
‘Background’, ‘Title and Controls’, ‘Online servers’¶
Draws some background colors and some text, not important
‘Servers (Loop)’¶
This draws the actual server list.
Let’s analyze it:
///Servers (Loop)
var l = global.udphp_downloadlist;
for (var i = 0; i<4;i++) {
draw_text(10,85+80*i,"=("+string(i+1)+")=");
First, the list global.udphp_downloadlist
is stored in the local
variable l
(because it’s shorter). This ds_list contains all the
servers we got from the master server.
Then it begins a loop that loops through the first 4 servers in the list we got by the master server, everything is in this loop and then it draws a nice little number for each server.
if (ds_exists(l,ds_type_list)) {
if (ds_list_size(l)>i) {
Now, this is the interesting part.
First we check if the downloadlist was already created (it get’s created
once the list has been downloaded). After that we check if it has at
least as many entrys as the server we want to list. For this example we
assume i
is 1. That means it checks if there is atleast one server
in the list. If yes, we have an entry we can now draw.
//Get stuff from the downloadlist
var entry = l[| i];
var ip = entry[? "ip"];
var game = entry[? "data1"];
var servername = entry[? "data2"];
var description = entry[? "data3"];
Now the entry (a ds_map) for our server is extracted from the list and we get the gamename, which is stored in data1, the ip, which is stored in the key “ip”, the name of the server, which we stored in data2, and so on.
draw_text(70,85+80*i,servername+" | "+ip);
draw_text(70,115+80*i,description);
}
}
draw_line(0,160+80*i,room_width,160+80*i);
}
Now we just draw everything.
The press 1-4 key events¶
Pressing 1-4 on the keyboard will connect to that game. Let’s see how!
///LOAD GAME SERVER ON SLOT 1
var l = global.udphp_downloadlist;
if (ds_exists(l,ds_type_list)) {
if (ds_list_size(l)>0) {
var entry = l[| 0];
var ip = entry[? "ip"];
var game = entry[? "data1"];
We again open the downloadlist and check if server 1 is in it, if yes we continue.
if (game != self.game) {
//Not compatible game, exit
show_message("Game server or version is incompatible!");
exit;
}
Remember the filtering variable we created in the create-event? We use it here to check if the server is a GMnet ENGINE demo game. If not we cancel. Please note, that this is propably not needed here, since we filtered out all, but our game in the create event, when we ran udphp_downloadServerList.
//====GMnet PUNCH DEMO ONLY
if (!script_exists(asset_get_index("htme_init"))) {
//This code is irrelevant for GMnet ENGINE and has been removed
}
//====GMnet ENGINE DEMO ONLY
else {
//Setup client, on success go to waiting room, otherwise end game
//We don't actually know the port, but that doesn't matter, the master
//server will tell us the port when we connect
//
//THE LINE BELOW is equivalent to:
//if (htme_clientStart(ip, 0)) {
if (script_execute(asset_get_index("htme_clientStart"),ip, 0)) {
//Wait for connection success!
room_goto(asset_get_index("htme_rom_connecting"));
} else {
show_message("Could not start client! Check your network configuration!");
}
}
} else {
//Do nothing - There is no server on this slot
}
} else {
//Do nothing - There is no server on this slot
}
This is the rest of the script. We once again check if we are running the GMnet ENGINE demo project and then we begin connection.
We call htme_clientStart
with the ip we got from the list and leave
the port at 0. Why? Because we don’t know the port. But that doesn’t
matter, because when connecting to the server, GMnet ENGINE will
automatically resolve the port using UDPHP.
Afer that we just go to the waiting room. Done! The rest is the default client connection you know and love.
And this is how you create a lobby! Now go ahead and do it! :)
How can my clients get the gamename and data strings after they connected?¶
Now, you might want to get the datastrings and the gamename after you connected. For example to display the name of the server in a corner, or to make sure the server is comaptible to the client when connecting manually.
For this you just create a object like the time syncing object. You sync it with the engine, so all other players get it and sync 8 variables that contain the value of the gamename string and the 7 data strings.
If you need help with that, contact me :)
Anything missing?¶
If this bonus chapter lacks something important, please let me know. If you have any problems feel free to contact me. I know this is quite complicated compared to the rest, so don’t be frustrated when you have problems.
Bonus: Global Sync¶
The update to version 1.1 added a new feature called “Global Sync”.
Before you learned how to sync instances between games in this tutorial. However this concept has one downside: Instances are only created and controlable by one client. All variables of an instance are read-only to all other clients. That’s why we needed to create a seperate chat instance for each player that basicaly all jut synced one chat variable. That may be fine for a few variables, and in this case maybe even the best way of doing it, since every player still needs one “chat outbox”, however you can now sync global variables that are read- and writeable by all clients at all times.
Summary of Global Sync’s features: Change the value on one end and it will update on all other ends as well. All ends can update and access all variables
How it works¶
It’s super simple. To store a variable in the Global Sync pool use this little code:
var value = 1+1;
htme_globalSet("name",value,buffer_u8);
The third argument is the Buffer type/data type.
This will now sync to all clients immediately (using the SMART syncType.).
You can retrieve this variable via this code on all connected clients and the server:
var value = htme_globalGet("name")
Please note that this function may return undefined
if it wasn’t set
by any of the clients/server yet, so check if it is defined:
if (is_undefined((htme_globalGet("name")) {
//Nope, that wasn't set yet...
}
Bonus: A LAN lobby¶
Note
This topic requires GMnet PUNCH enabled.
*NOTE: In addition to LAN lobbys, there are also ONLINE lobbys!
In addition to online lobbys, you can now also create LAN lobbys. In this tutorial we will explain how. We’ll assume you haven’t read the online lobby tutorial, but if you have: Creating a LAN lobby is similiar, but there are some key differences.
This part of the tutorial will explain everything you’ll need to know. Before we start, please create a new project and load the sample project (add all files from the GMnet ENGINE asset). ###Testing the lobby
When you press K when starting the game you will be brought to the demo lobby room. This is a very simple demo room that can only show four servers. Once we are done, you’ll be able to create an even better lobby.
To test it, fire up a demo game on another PC in the network and start a server. You’ll see the server in the list after a few seconds.
The new concepts: Gamenames¶
(This is the same text as in the online lobby tutorial)
In htme_config
there is a new variable you need to change when
creating your own game (keep the default for the demo project though,
otherwise you can’t join demo servers via the lobby):
/**
* Shortname of this game
* + version
* Example: htme_demo100
**/
self.gamename = "htme_demo100"
This string represents your game and the version of your game. If you update your game and it’s server is not compatible with older versions, change this string and you can prevent players from joining older servers in the lobby (Please note that this is only for the lobby, if your players are able to connect manually, you need to implement your own way of kicking him out of the game again after he has connected, see How can my clients get the gamename and data strings after they connected? below for more info)
You can change it at any time, for example if you have multiple
gamemodes you want to mark in the lobby via htme_setGamename(name)
and get it via htme_getGamename()
.
Data strings¶
(This is the same text as in the online lobby tutorial)
Asside from the gamename string, there are 7 more strings that can be used to identify your game in the lobby.
Start the demo game and create a server. You will be asked to enter the name of the server and a description. These are data strings 2 and 3. The demo lobby uses them for these purposes, but when making your own game, you can use them however you want. 4-8 are unused by the demo project.
Let’s open the N-key event of htme_obj_menu (this event creates the server).
You’ll find this part after the server has successfully been created:
htme_setData(2,get_string("How should this server be called (for the lobby)?","GMnet ENGINE Demo Server"));
htme_setData(3,get_string("Enter a server description (for the lobby)?","A server created for the GMnet ENGINE demo project"));
As you can see, we set the server name and description right after the
server was started using htme_setData(n,string)
. We use 2 for server
name and 3 for description. You can also on the server end retrieve
these strings using htme_getData(n)
.
If you want to update any data string later, after the server connected
to the master server, you need to use htme_commitData()
. This syncs
all data strings to the master server. You MUST NOT use it here, because
the server hasn’t connected to the master server yet!
A note for GMnet PUNCH users: The gamename is data string 1.
Setting broadcast settings.¶
By default the servers will broadcast their information to all PCs in
the LAN each 15 seconds. You can change this in htme_config
by
changing the value of self.lan_interval
/**
* Interval the servers broadcast data to the LAN, for the LAN lobby
* @type real
*/
self.lan_interval = 15*room_speed;
Building the lobby¶
Ah, the lobby. Finally we are ready to build it.
The room of the lobby is the room htme_lanlobby
. This room only
contains the object htme_obj_lanlobbydemo
. This object controls the
lobby. Let’s dive into it!
The create event¶
self.game = "htme_demo121"
//IF YOU USE YOUR OWN SERVER - Change self.game!
///Recieve lobby data from the master server
htme_startLANsearch(real(get_string("On which port should we search for servers?","6510")),self.game);
This will start searching the LAN for games with the game id
self.game
on the port that is asked to the player. More information
on this function can be found in the usage page of
htme_startLANsearch.
The Room end event¶
///STOP LAN SEARCH
htme_stopLANsearch();
This will stop searching for LAN servers. You should run this if your player leaves the lobby
The networking event¶
///LOOKING FOR INCOMING SERVER BROADCASTS
//htme_step doesn't do that btw!
htme_networking_searchForBroadcasts();
This runs the code that actually searches for LAN servers. It waits for incoming server broadcasts.
The draw event¶
The draw event is split up into different sub-scripts:
‘Background’, ‘Title and Controls’, ‘Online servers’¶
Draws some background colors and some text, not important
‘Servers (Loop)’¶
This draws the actual server list.
Let’s analyze it:
///Servers (Loop)
var l = htme_getLANServers();
for (var i = 0; i<4;i++) {
draw_text(10,85+80*i,"=("+string(i+1)+")=");
First, the list htme_getLANServers()
is stored in the local variable
l
(because it’s shorter). This ds_list contains all the servers
we found. It will be filled over time with all servers in the LAN.
Then it begins a loop that loops through the first 4 servers in the list we got by the master server, everything is in this loop and then it draws a nice little number for each server.
if (ds_exists(l,ds_type_list)) {
if (ds_list_size(l)>i) {
Now, this is the interesting part.
Ee check if it has at least as many entrys as the server we want to
list. For this example we assume i
is 1. That means it checks if
there is atleast one server in the list. If yes, we have an entry we can
now draw.
//Get stuff from the downloadlist
var entry = l[| i];
var ip = entry[? "ip"];
var port = entry[? "port"];
var game = entry[? "data1"];
var servername = entry[? "data2"];
var description = entry[? "data3"];
Now the entry (a ds_map) for our server is extracted from the list and we get the gamename, which is stored in data1, the ip, which is stored in the key “ip”, the name of the server, which we stored in data2, and so on.
draw_text(70,85+80*i,servername+" | "+ip+":"+string(port));
draw_text(70,115+80*i,description);
}
}
draw_line(0,160+80*i,room_width,160+80*i);
}
Now we just draw everything.
The press 1-4 key events¶
Pressing 1-4 on the keyboard will connect to that game. Let’s see how!
///LOAD GAME SERVER ON SLOT 1
var l = htme_getLANServers();
if (ds_exists(l,ds_type_list)) {
if (ds_list_size(l)>0) {
var entry = l[| 0];
var ip = entry[? "ip"];
var game = entry[? "data1"];
We again open the downloadlist and check if server 1 is in it, if yes we continue.
if (game != self.game) {
//Not compatible game, exit
show_message("Game server or version is incompatible!");
exit;
}
Remember the filtering variable we created in the create-event? We use it here to check if the server is a GMnet ENGINE demo game. If not we cancel. Although this is not needed if you filtered out other games with htme_startLANsearch like we did in the create event.
if (htme_clientStart(ip, port)) {
//Wait for connection success!
room_goto(htme_rom_connecting);
} else {
show_message("Could not start client! Check your network configuration!");
}
} else {
//Do nothing - There is no server on this slot
}
} else {
//Do nothing - There is no server on this slot
}
This is the rest of the script. We call htme_clientStart
with the ip
we got from the list the port.
Afer that we just go to the waiting room. Done! The rest is the default client connection you know and love.
And this is how you create a lobby! Now go ahead and do it! :)
Anything missing?¶
If this bonus chapter lacks something important, please let me know. If you have any problems feel free to contact me. I know this is quite complicated compared to the rest, so don’t be frustrated when you have problems.
Bonus: Event Handlers for Connecting/Disconnecting¶
Basic example¶
Let’s say you have this script called scr_welcome
:
show_message("Cool! Someone connected!");
return true; //- We will come to that later
You want to execute this script, if someone connects. To do that simply use this code somewhere after the engine was started (obj_htme is created):
htme_serverEventHandlerConnecting( scr_welcome );
That’s all you have to do. For a script to be run at disconnection,
simply call htme_serverEventHandlerDisconnecting
instead.
What is the return value for?¶
As you saw in the example above, the script scr_welcome
returns
true
.
If you are using a script as an event handler for connecting players, you need to return either true or false (you don’t have to return anything for the Disconnect-Event).
Getting more information about the player¶
Both, the disconnect and connect event, provide an argument for your script.
Here is an example how you can use this information. The server will refuse all connections from the local computer when using this script:
///somewhere in your code
htme_serverEventHandlerConnecting( scr_no_local_clients );
htme_serverEventHandlerDisconnecting( scr_goodbye );
///scr_no_local_clients(player_map);
var player_map = argument0;
if (player_map[? "ip"] == "127.0.0.1") return false;
else {
show_message("Cool! "+player_map[? "ip"]+":"+string(player_map[? "port"])+" connected!");
return true;
}
///scr_goodbye(player_map);
var player_map = argument0;
show_message(":( ! "+player_map[? "ip"]+":"+string(player_map[? "port"])+" with the hash "+player_map[? "hash"]+" left!");
Bonus: RPC¶
RPCs (Remote procedure calls) are used to execute scripts/code/functions remotely.
For the engine this means RPC enables you to execute scripts on other clients / servers. After you finished this part of the tutorial you will have a fully functional way of calling scripts via the network.
This tutorial will also show you how to send the value returned by the RPC-called script back to the game of the player who called it.
WARNING: Please note that this tutorial does not cover any secruity mechanisms. You propably want to restrict which scripts can be executed and who and when clients can execute them and on which targets.
To make RPCs with GMnet ENGINE we will be using the CHAT Interface. Basic knowledge on how to use that is required.
**This section is more advanced and good coding skills are required [ don’t fear to try it out though :) ] **
Setup¶
First we need an object to listen for RPCs and to send RPCs through.
Create an object obj_htme_rpc with the events below, make it persistent and make sure it is created when clients have connected or a server was started, and get destroyed when the engine shuts down.
Create Event¶
mp_syncAsChatHandler("RPC");
Step Event¶
var queue = mp_chatGetQueue();
while (ds_queue_size(queue) > 0) {
htmerpc_recieve(ds_queue_dequeue(queue));
}
And create the two scripts ``htmerpc_recieve`` and ``htmerpc_send``. The will be used to send and recieve RPC calls. And also create the script ``htmerpc_callback``. This script will later be used to store the values that the RPC-called scripts return.
The RPC protocol¶
We will now create a protocol to send and recieve the RPCs.
The message¶
Okay, so let’s think about how we can “package” our RPCs.
Each RPC needs to contain a script name and it’s argument. And each RPC has one recipient that should execute the command.
Let’s use a ds_map for this. GameMaker allows you to convert ds_maps into JSON which can be sent over the network and then be decoded again.
Our message will be a ds_map, turned into a json string with the following contents:
ds_map =>
[id] => string
[script] => string
[argument_count] => real
[argument0] => string or real (optional)
[argument1] => string or real (optional)
...
[argument12*] => string or real (optional)
id
is used to identify the RPC calls. You will see why this is
important later.
When we later convert these ds_maps into JSON, the message will look like this:
{"id":"foo","script":"scr_myscript","argument_count":2,"argument0":12.3,"argument1":"Hello World"}
* Only 13 arguments can be used as the script htmerpc_send
we will
create now can only take in 16 arguments, and two of them are used for
other things
Creating a script to send messages¶
htmerpc_send
. This script should be used
anywhere to send RPC commands.So let’s fill the script with this:
///htmerpc_send(id,script,to,[argument0...13])
/* Sends RPC calls to another player */
/*
* Turn script into a string. You call this function with (htmerpc(my_script,...);
* my_script will be turned into a number by Game Maker that identifies this script,
* we turn this into a string to send it over the network, because if you use
* different versions of your game, this id might not be the same for the same script.
*/
var rid = argument[0];
var script = script_get_name(argument[1]);
var to = argument[2]; //Hash of the player to send this to
var rpc_argument;
if (argument_count > 3) rpc_argument[0] = argument[3];
if (argument_count > 4) rpc_argument[1] = argument[4];
if (argument_count > 5) rpc_argument[2] = argument[5];
if (argument_count > 6) rpc_argument[3] = argument[6];
if (argument_count > 7) rpc_argument[4] = argument[7];
if (argument_count > 8) rpc_argument[5] = argument[8];
if (argument_count > 9) rpc_argument[6] = argument[9];
if (argument_count > 10) rpc_argument[7] = argument[10];
if (argument_count > 11) rpc_argument[8] = argument[11];
if (argument_count > 12) rpc_argument[9] = argument[12];
if (argument_count > 13) rpc_argument[10] = argument[13];
if (argument_count > 14) rpc_argument[11] = argument[14];
if (argument_count > 15) rpc_argument[12] = argument[15];
This “simply” processes the arguments for the script. All arguments for the RPC script get written in the rpc_argument array. You can also do this using a loop by the way, which is far more elegant. We will use argument_count later again to see how many RPC arguments we have.
We actually don’t have that much to do now. We create the ds_map...
var rpc_command = ds_map_create();
...fill it...
rpc_command[? "id"] = rid;
rpc_command[? "script"] = script;
//If argument_count is 4, we have 4 arguments, which means 1 rpc argument etc.
var rpc_argument_count = argument_count-3;
rpc_command[? "argument_count"] = rpc_argument_count;
//Now we loop through all arguments and add them to the list:
for (var i = 0; i < rpc_argument_count; i++) {
rpc_command[? "argument"+string(i)] = rpc_argument[i];
}
...and convert it to json:
var message = json_encode(rpc_command);
//After that we don't need the map anymore
ds_map_destroy(rpc_command);
That’s all. We can now send the message.
mp_chatSend allows a second argument
called to
. This is the hash of the player that should recieve the
message. Exactly what we need! We use a with-Block to call the
mp_chatSend
with our RPC object
with (obj_htme_rpc) {
mp_chatSend(message,to);
}
The message is sent! Let’s process it!
Recieving RPCs¶
What’s the point of sending RPCs if we can’t recieve them, right?
In the step event we created earlier we already added a call to
htmerpc_recieve
with a message as argument. Let’s create the script.
///htmerpc_recieve(message)
/* Processes RPC calls*/
var message = htme_chatGetMessage(argument0);
var from = htme_chatGetSender(argument0);
Using htme_chatGetMessage we take the raw message and decode it to get the actual message. We also store the hash of the player that sent the RPC, because at the end of this script we want to send a RPC back containing the returned value of the script. But we’ll come to that in a bit.
Let’s decode the ds_map:
var rpc_command = json_decode(message);
rpc_command
will now contain the ds_map we created earlier. Magic!
Let us waste no time and execute the command. This can be done by
combining asset_get_index
and script_execute
.
asset_get_index
turns the string that contains the script name back
into an id and script_execute
executes the command using this id.
Because we also want to process the arguments, this may look a bit ridiculous now, but it works:
var rid = rpc_command[? "id"];
var rpc_argument_count = rpc_command[? "argument_count"];
var result;
if (rpc_argument_count == 0) {
result = script_execute(asset_get_index(rpc_command[? "script"]));
}
if (rpc_argument_count == 1) {
result = script_execute(asset_get_index(rpc_command[? "script"]),rpc_command[? "argument0"]);
}
if (rpc_argument_count == 2) {
result = script_execute(asset_get_index(rpc_command[? "script"]),rpc_command[? "argument0"],rpc_command[? "argument1"]);
}
/** CONTINUE THIS UNTIL 14 **/
if (rpc_argument_count == 13) {
result = script_execute(asset_get_index(rpc_command[? "script"]),rpc_command[? "argument0"],rpc_command[? "argument1"],rpc_command[? "argument2"],rpc_command[? "argument3"],rpc_command[? "argument4"],rpc_command[? "argument5"],rpc_command[? "argument6"],rpc_command[? "argument7"],rpc_command[? "argument8"],rpc_command[? "argument9"],rpc_command[? "argument10"],rpc_command[? "argument11"],rpc_command[? "argument12"]);
}
And well... that’s it. But wait - now we need to send the value that this function returned back.
Returning the value and sending it back¶
Everything we did so far was fine, but now what if we want to know what the script returned?
Remember the script htmerpc_callback
we created earlier? When we are
done executing the RPC script we send the returned value we stored in
result
above back to the sender of the RPC. This script will then
tell the author of the original RPC the returned value.
That means:
- Player A sends RPC to Player B to run scr_myscript which returns “Hi!”.
- Player B recieves the RPC, runs the script and stores the value in
result
. - Player B sends RPC to Player A to run
htmerpc_callback
with the arguments being theid
of the original call and theresult
. - Player A recieves the RPC, runs the script, notices that it calls
htmerpc_callback
and therefor stops (otherwise this would be an endless loop).
Step 2 and 4 in script (add to the end of htmerpc_recieve
):
//Only send returned value if this RPC isn't already about a returned value (otherwise this would result in an endless loop)
if (rpc_command[? "script"] != "htmerpc_callback") {
//Send returned value back via RPC
//The id is not relevant for this because we don't track this RPC - we leave it empty.
htmerpc_send("",htmerpc_callback,from,rid,result);
}
//Destroy the map, we don't need it
ds_map_destroy(rpc_command);
The recieve-Script is now done. We now need to create
htmerpc_callback
and add a way to actually get the values.
Storing and retrieving the returned values¶
But before we do that, we need somewhere to store those values. Let’s
use our obj_htme_rpc
for that and a ds_map. It will have the ids as
keys and the returned values as values.
Add it to the create event:
self.returnedValues = ds_map_create();
Okay, so now let’s create htmerpc_callback
, this script is actually
incredibly simple:
///htmerpc_callback(id,returnedValue)
/* Reciveves the returned value of RPCs via RPC */
ds_map_add(obj_htme_rpc.returnedValues,argument0,argument1);
That is all. Whenever a RPC call is sent, it will now add the returned value to the map. You can get it anywhere by using code like this:
///Create event of some object - You can use htme_hash() to generate random ids
self.rpc_id = htme_hash();
htmerpc_send(self.rpc_id,my_cool_script,some_player_hash,0,"Test",3+2);
///Step event
var returnedValue = ds_map_find_value(obj_htme_rpc.returnedValues,self.rpc_id);
//ds_map_find_value returns undefined if the key was not found -> if the returnedValue has not been recieved
//Please make sure the function actually returns something and returns something other than undefined, otherwise this code will never run.
if (!is_undefined(returnedValue)) {
show_message(returnedValue);
//After that make sure to delete the key if you don't need it anymore
ds_map_delete(obj_htme_rpc.returnedValues,self.rpc_id);
}
When using this for your game, be sure to include error handlers and a secuity mechanism as explained at the top of the page.
Concepts¶
Buffer types¶
When using mp_add you need to specify a buffer type that specifys as which datatype the information is sent. This is set for all variables in the Variable Groups.
The buffer types are the same as the ones used by Game Maker.
Booleans (Reals with 0/1; false/true)¶
buffer_bool
Reals (Numbers)¶
From the Game Maker manual:
Type | Desciption |
---|---|
buffer_u8 | An unsigned, 8bit integer. This is a positive value from 0 to 255. |
buffer_s8 | A signed, 8bit integer. This can be a positive or negative value from -128 to 127 (0 is classed as positive). |
buffer_u16 | An unsigned, 16bit integer. This is a positive value from 0 - 65,535. |
buffer_s16 | A signed, 16bit integer. This can be a positive or negative value from -32,768 to 32,767 (0 is classed as positive). |
buffer_u32 | An unsigned, 32bit integer. This is a positive value from 0 to 4,294,967,29 5. |
buffer_s32 | A signed, 32bit integer. This is a positive value from 0 to 264 - 1. |
buffer_u64 | An unsigned 64bit integer. This can be a positive or negative value from -(263) to 263 - 1. |
buffer_f32 | A 32bit float. This can be a positive or negative value within the same range as a 32 bit signed integer. |
buffer_f64 | A 64bit float. This can be a positive or negative value from -(263) to 263 - 1. |
Floats are numbers with commas (3,2), Integers are numbers without (3). When syncing a number with commas as an Integer, the value will be rounded.
CHAT Interface¶
The CHAT (Custom Handler for Advanced Traffic) Interface is a new way of syncing information between players.
Instead of using objects and instances, this uses a more classic approach. Using the CHAT Interface you can send and retrieve string messages, with the support for multiple channel.
CHAT messages are sent using the IMPORTANT sync type.
Tutorial¶
Commands¶
- mp_syncAsChatHandler for registering an object to recieve and send messages on a channel.
- mp_chatGetQueue to recieve new messages.
- mp_chatSend to send messages.
- htme_chatGetMessage to get the message out of a message.
- htme_chatGetSender to get the author of a message.
The debug overlay¶
The debug overlay is a new feature that allows you to do better
debugging. Enable it by setting self.debugoverlay
to true in
htme_config.
F12: Toggle overlay¶
F12 will show and hide the overlay when it’s enabled.
F1: All instances¶
Shows a list of all instances and the variable groups for local instances.
For the first 10 instances in the list, press SHIFT + the shown number to access instance details.
Instance details¶
Variable | Descripti on |
---|---|
Hash | The id of the instance as stored by the engine |
isVisible | Whether or not this is a visible instance |
isNotCach ed | True if this instance is in the same room (server only; clients will only show instances in the same room) |
Persisten t | See Instance scrope and rooms |
stayAlive | See Instance scrope and rooms |
Instance ID | The local id of the instance, assgined by GameMaker . If cached the instance will not actually exist, and therefor the instance will have no id |
Object | Name of the object of this instance |
Player | Number and hash of the player this instance belongs to |
Saved variables | All variables as synced by the engine |
Vargroups | Details of all `variable groups <c oncepts/v argroups> `__ |
F2: Visible instances¶
Same as F1 but filters by showing only visible instances
F3: Players¶
Shows a list of all players, including their player number and their hash. Also shows who is the local player and who runs the server.
Press SHIFT + number shown to show all instances of one player. If you are the server, press STRG + number to kick a player.
F4: Invisible instances¶
Same as F1 but filters by showing only invisible instances
F5: Instances in cache¶
Same as F1 but showing only cached instances (instances in same room; This list is always empty for clients).
F6: Global sync¶
Shows all variables in the global sync pool.
F7: CHAT Interface Channels¶
Lists all CHAT Interface channels and how many messages haven’t been processed (read) and what the content of the most recent unread message is.
F8: Signed packets sent¶
Shows details about all signed packets that are not yet recieved by the clients/server.
F9: Signed packets inbox¶
Shows details about all signed packets that have been sent to the clients/server.
F10: Maps and Lists¶
Shows a count of all ds_lists and ds_maps. If the numbers just increase it may be a memory leak somewhere.
F11: Disconnect - Client only¶
Disconnect from the server. This will tell the server to kick us and then kill the engine.
Check if overlay is enabled¶
htme_debugOverlayEnabled
will return true when the overlay is
enabled. Use this to draw your own debug stuff when the overlay is
active.
Delta time and room_speed¶
The engine can use room_speed
(default) or delta time.
room_speed¶
The engine is set to use room_speed
as the default timer count.
You use it when you sync objects:
mp_addPosition("Pos",5*room_speed);
mp_add("playerName","name",buffer_string,60*room_speed);
In htme_config:
self.udphp_rctintv = 3*60*room_speed;
self.global_timeout = 5*room_speed;
self.punch_stage_timeout=1*room_speed;
self.lan_interval = 15*room_speed;
Room speed is simple to use, understand and works most of the time.
room_speed limits¶
Game Lag
If your game experience lag the
room halt and so do the room speed count. Lets say that you are connected to a server and your game lag for 3 seconds. Note that the global_timeout
is set to 5*room_speed
= 5 seconds. You and the server got each a counter running. And these counters is not synchronized. Server counter can be on 3 and yours on 1.
This means that your game will disconnect if the server don’t respond within 5 seconds.
But it also means that the server will kick you if you don’t send anything within 5 seconds.
The server got his eyes on his watch and if your lag is on his 3 second count you will be kicked.
Because 3+3 lag=6 seconds.
Different room speeds
Another issue is that your game must have the same room speed in all rooms. Because if you are in a room with speed 30. And the server is in a room with speed 60. Every second for you is 2 seconds for the server. So you will be kicked. Because 3*2=6. So after 3 seconds on your counter the server will be on 6 and kick you.
Delta time¶
Delta time can compensate for lag and different room speed. Because it counts real seconds. If your game lag for 3 seconds. The counter is still counting.
Delta time setup¶
You need to setup the engine to use delta time. This means that all timers must be changed to real seconds. When using room speed you set:
mp_addPosition("Pos",5*room_speed);
But you need to change it to:
mp_addPosition("Pos",5);
This set the time to 5 seconds. Note: Sometimes you set the time just to 3 to update the position every 3 steps. But if you activate delta time this means 3 seconds now. So say your room speed is 30 you must change it to 0.1. Because 3 steps in a room with room_speed 30 is 0.1 seconds.
But we need to set this up. First go to the script ``htme_config``and set:
use_delta_time=true;
You also need to remove all the room_speed
in htme_config
.
self.udphp_rctintv = 3*60;
self.global_timeout = 5;
self.punch_stage_timeout=1;
self.lan_interval = 15;
And go throught all your synced object and remove the room_speed
.
mp_addPosition("Pos",5);
mp_add("playerName","name",buffer_string,60);
GMnet GATE.TESTER - The master server testing tool¶
GMnet GATE.TESTER allows you to test your master servers.
It’s currently basicly an online lobby browser for debugging. Run your
master server with the --testing
parameter to enable debugging using
this tool.
You can find it at: http://gmnet.parakoopa.de/gatetester
Local and remote Instances¶
When you place an instance of an object into a room, that is set up to be be synced via GMnet ENGINE (mp_sync(); called in the create-Event), you will have one local instance of this object in your room, and one remote instance for each other player you are connected to.
That means,** if you place one instance of object A in your room, you will have 4 instance if 4 players are playing**. Each player has **one local instance he controls**, and the **three other instances that the other players control**.
You can find out if a instance is local by using htme_isLocal(); by using this if-Statement:
if (htme_isLocal()) {
//Bla
}
everything in the statement will only be executed once for every player, by the instance that he controls.
If you place 2 instances of an object into the room, you will have 8 instances with 4 players, of which 2 are local and the above code will be executed by these two instances.
Remember that all code outside the htme_isLocal
will run on the remote computer.
This is because when the htme_obj_player
sync to the other computer his game will create the same object.
So the create event, step event all events will run on his computer too. So you must make sure that you only run what you
want on the remote computer.
Ex in the htme_obj_player
you have this code and this will run when you start the game
// This code will run on your computer
// *************************
self.pressed_jump = false;
self.pressed_left = false;
self.pressed_right = false;
self.name = "";
// *************************
// This code will run on your computer
// *************************
if (htme_isLocal()) {
var ttr = totro(5,7,1);
self.name = ttr[0];
self.image_blend = irandom(16777215);
}
// *************************
The htme_obj_player
is synced to all other player and will be created with instance_create(x,y,htme_obj_player)
.
And you know that when you create an object all events will run. So this will run on the remote computer:
// This code will run on the remote computer
// *************************
self.pressed_jump = false;
self.pressed_left = false;
self.pressed_right = false;
self.name = "";
// *************************
// This code will not run because htme_isLocal()=false with a remote object on a remote computer
// *************************
if (htme_isLocal()) {
var ttr = totro(5,7,1);
self.name = ttr[0];
self.image_blend = irandom(16777215);
}
// *************************
The other players will also sync their htme_obj_player
to you so this will run on your computer:
// This code will run on your computer
// *************************
self.pressed_jump = false;
self.pressed_left = false;
self.pressed_right = false;
self.name = "";
// *************************
// This code will not run because htme_isLocal()=false on your computer with a remote object
// Your computer is a remote computer in the other players perspective
// *************************
if (htme_isLocal()) {
var ttr = totro(5,7,1);
self.name = ttr[0];
self.image_blend = irandom(16777215);
}
// *************************
As you can see there is a perspective. In your case all other players object is a remote object and you are local. But in all other players perspective you are the remote and they are the local.
mp_map_syncIn and mp_map_syncOut¶
Whenever you use mp_add, you need to do 2 things for each variable you sync with it:
- Add the following to the end step event:
gml self.VARIABLENAME = mp_map_syncOut("VARIABLENAME", self.VARIABLENAME);
Where you replace “VARIABLENAME” with the name of your variable. - Run the following code whenever you change that variable, we
recommend doing this in the begin step event:
gml mp_map_syncIn("VARIABLENAME",self.VARIABLENAME);
This also needs to be done at least once after you set up the engine if you don’t put it in begin step.
THIS DOES NOT TO BE DONE FOR ANY OTHER FUNCTION STARTING WITH mp_add. ONLY WITH mp_add ITSELF!
Technical explanation¶
The engine needs to know what variables it needs to sync. So if you execute this code:
mp_add("message","message",buffer_string,5);
Then we know you want to sync the variable message. The problem/limitation in Game Maker is, that we can’t get the value of the message variable of this instance now. Why?
Well let me have an example. This works:
var instance; //Some valid instance
var test = (instance).x;
//Test will now have the x position of the instance as value
Now in that case we are directly accessing the x-Position of the instance. That’s what all other mp_add* functions besides mp_add itself do, because they can be hardcoded. When writing the code we already know what to sync and in what order.
We don’t know how your variables are called while coding, so we have to get them dynamically. Let’s take the following exmaple:
var instance; //Some valid instance
var variable = "x";
//What now???
var test = (instance).variable; //? Doesn't work!
var test = (instance).(variable); //? Doesn't work!
var test = (instance)[variable]; //? Doesn't work!
There’s no way in Game Maker Studio of doing that. That’s why we use a ds_map to cache your instances variables. Because with a ds_map, that does work!
var map; //Some valid ds_map that belongs to your instance; That's what you change when using the syncIn and syncOut command
var variable = "x";
var test = ds_map_find_value(map,variable); //Yay! Works!
PLUS - Master server (GMnet GATE.PUNCH)¶
See this page of the tutorial:
**PLUS** - Hosting a master server
A detailed description of how GMnet PUNCH works, can be found in the manual of GMnet PUNCH.
Players and Playerhashes¶
Players are identified by hash strings.
You can loop through all players using this loop over the list returned by htme_getPlayers:
var playerlist = htme_getPlayers();
for(var i = 0;i<ds_list_size(playerlist);i++) {
var player = ds_list_find_value(playerlist,i);
}
player
will contain the hash of the player.You can get the local player (your player) hash by using the variable global.htme_object.playerhash
.
You can get a player hash from a synced object by using the objects variable obj_name.htme_mp_player
.
Instance scope and rooms¶
Non-persistent instances will disappear if they leave the room. It doesn’t matter if a local instance or a remote instance leaves the room, it will always disappear.
Persistent instances will follow the local player into the next room. That means persistent remote instance will be destroyed, persistent local instances not.
stayAlive instances (objects where mp_stayAlive(); was used) that are also persistent will always follow the local player into a new room, no matter if it’s a local or remote instance.
Here’s a table that illustrates that:
INSTANCE WAS CREATED BY PLAYER A IN ROOM A
Signed packets¶
Signed packets are a special type of packets that are guaranteed to arrive.
Normally when using UDP multiplayer, packets are sent very fast on the cost of reliability.
Signed packets give GMnet ENGINE the option to have packets that are sent with the reliability of TCP. These are used with the sync types SMART and IMPORTANT and some other internal operations.
You can also use them. Either by using the SMART and IMPORTANT sync types or by using them with your own packets. For how see “Extending the Engine / Advanced Use”.
States of the engine¶
The engine can be in different states.
- Is the engine running (=Server or Client started)? -> htme_isStarted returns true
- Are we running the server? -> htme_isServer returns true
- (Only client) -> Is the client connected to a server? -> htme_clientIsConnected returns true
- (Only client) -> Did a connection to the server fail? -> htme_clientConnectionFailed returns true
VarGroup SyncTypes (mp_type)¶
Each Variable Group can be assigned one
syncType using mp_setType that controls
how the varGroup will be synchonized. They are enum values of the enum
mp_type
. FAST actually means mp_type.FAST
! The default value, if
you don’t use mp_setType, is mp_type.FAST
.
These are your options:
- FAST: The engine sends the variable once in the interval you set. Since we are using UDP for networking, there is no assurance, that it will actually arrive. That means we don’t know if the other players actually get this change. The packet could get lost. However, using FAST is very fast.
- IMPORTANT: When using IMPORTANT, the variable will still be synced in the set interval. However, we are using a special way in GMnet ENGINE to make sure the packet arrives. Every X seconds, the server/client will flood the other players with the information, until they respond back, that they got it. That’s how we assure the information is actually sent. This is however a pretty traffic heavy operation and using that in too short intervals will cause lagg in both connection and framerate.
- SMART: This is like IMPORTANT but better. Instead of syncing every X seconds, we will only sync every X seconds what has changed. This is basicly a more traffic-friendly version of IMPORTANT.
Tolerance¶
Using mp_tolerance you can give each variable that is a real (a number) a tolerance range in that it will not be chanegd locally. This is mainly used to make smooth movement and prevent players from jumping arround a few pixels.
Take as an example the x position:
If the actual x-Position of an instance should be 12 but it’s 15 locally, the change will not be applied locally if the tolerance is greater than or equal 3.
PLUS - GMnet PUNCH¶
See this page of the tutorial:
A detailed description of how udphp works, can be found in the manual of GMnet PUNCH.
UPnP¶
UPnP stands for Universal Plug and Play.
GMnet ENGINE use UDP hole punching to make the NAT/router allow online connections.
UPnP automates the port forwarding on the users router or NAT. It sends a command to the router to forward a port.
But not all NATs allow UDP punch. The user must then manually port forward the server port (ex 6510) or use other methods such as UDP Hole-Punching (as done by GMnet PUNCH) that can also allow online play without UPnP or port forwarding.
By enabling UPnP you allow users who don’t forward their port and that are in networks that don’t support UDP Hole-Punching to play anyway.
When your users start the server, UPnP will try to port forward the server port for them.
You can enable UPnP in htme_config
.
Note
UPnP won’t work 100% for everybody (even if UPnP is supported) when used with the GMNet PUNCH master server until version 1.4.0!
But there is no harm in turning it on anyway.
Functions¶
Sync Functions¶
mp_add(groupname,variables,datatype,interval)
¶
Description¶
Adds a new group of variables to be synced to this instance.
Please read this manual page when using mp_add: mp_map_syncIn and mp_map_syncOut
Example¶
///Create Event
mp_sync();
mp_add("playerName","name",buffer_string,10*room_speed);;
Arguments¶
Name | type | description |
---|---|---|
group name | strin g | The name of the group, this is only used locally to identify this group, for example if you want to use mp_setType |
varia bles | strin g | A list of local variables of the instance seperated with commas |
datat ype | real, buffe r_* | A value of a “buffer_” constant to specify the data type of all variables in this group. See manual for a list of datatypes and their meanings. All datatypes from enum mp_buffer_ types are also allowed but should not be used by you as a user! |
inter val | real | The interval in which the variable group get’s synced with the other players |
Returns¶
Nothing
mp_addBuiltinBasic(groupname,interval)
¶
Description¶
Adds a new group of variables to be synced to this instance.
The variables are: * image_alpha,image_angle,image_blend,image_index,image_speed, image_xscale,image_yscale,visible
Example¶
///Create Event
mp_sync();
mp_addBuiltinBasic("drawing",10*room_speed);;
Arguments¶
Name | type | description |
---|---|---|
group name | strin g | The name of the group, this is only used locally to identify this group, for example if you want to use mp_setType |
inter val | real | The interval in which the variable group get’s synced with the other players |
Returns¶
Nothing
mp_addBuiltinPhysics(groupname,interval)
¶
Description¶
Adds a new group of variables to be synced to this instance.
The variables are: * direction,gravity,gravity_direction,friction,hspeed,vspeed
Example¶
///Create Event
mp_sync();
mp_addBuiltinPhysics("physics",10*room_speed);
Arguments¶
Name | type | description |
---|---|---|
group name | strin g | The name of the group, this is only used locally to identify this group, for example if you want to use mp_setType |
inter val | real | The interval in which the variable group get’s synced with the other players |
Returns¶
Nothing
mp_addPosition(groupname,interval)
¶
Example¶
///Create Event
mp_sync();
mp_addPosition("pos",10*room_speed);;
Arguments¶
Name | type | description |
---|---|---|
group name | strin g | The name of the group, this is only used locally to identify this group, for example if you want to use mp_setType |
inter val | real | The interval in which the variable group get’s synced with the other players |
Returns¶
Nothing
mp_map_syncIn(varName, variable)
¶
Description¶
More information: mp_map_syncIn and mp_map_syncOut
Arguments¶
Name | type | description |
---|---|---|
varName | string | Name of the variable to store |
variable | any | Value of that variable |
Returns¶
Nothing
mp_map_syncOut(varName, variable)
¶
Description¶
More information: mp_map_syncIn and mp_map_syncOut
Arguments¶
Name | type | description |
---|---|---|
varNa me | strin g | Name of the variable to get |
varia ble | any | Value of the variable that the instance currently has. Will later be used to compare them to values in the engine. Currently this is returned if the engine has no valid data for this varName. |
Returns¶
Value of varName in the variable map of this instance
mp_setType(group,type)
¶
Description¶
Changes the type with which a group will be synced to the other clients.
More information: VarGroup SyncTypes (mp_type)
Example¶
///Create Event
mp_sync();
mp_addBuiltinPhysics("physics",10*room_speed);
mp_setType("physics",mp_type.SMART);
Arguments¶
Name | type | description |
---|---|---|
group name | strin g | The name of the group of which you want to change the type of |
inter val | real | The values of the enum mp_type can be seen in htme_config or the manual. |
Returns¶
Nothing
mp_stayAlive()
¶
Description¶
Configure this instance to be in stayAlive mode and be room independent. Instance NEEDS to be persistent!
More information: Instance scope and rooms.
Example¶
///Create Event
mp_sync();
mp_stayAlive();
Arguments¶
None
Returns¶
Nothing
mp_sync()
¶
Description¶
Configures this instance to be synced via the GMnet ENGINE. This must be called before all other mp commands.
Example¶
///Create Event
mp_sync();
mp_addPosition("Pos",5*room_speed);
Arguments¶
None
Returns¶
Nothing
mp_tolerance(groupname,tolerance)
¶
Example¶
///Create Event
mp_sync();
mp_addPosition("pos",1);
mp_tolerance("pos",10);
Arguments¶
Name | type | description |
---|---|---|
groupname | string | The name of the group |
interval | real | The tolerance to apply |
Returns¶
Nothing
mp_unsync()
¶
Description¶
Removes an instance from the engine and * if not [local]concepts/instances): destroys it [DO NOT USE IT ON REMOTE INSTANCES!] * if local: informs server which then informs all players.
Does NOT destroy local instances! You have to do that yourself when calling this manually!
Example¶
///Step
if (somethinghappend && htme_isLocal()) {
mp_unsync();
instance_destroy();
}
Arguments¶
None
Returns¶
Nothing
Tools¶
htme_clientConnectionFailed()
¶
Description¶
Check if the client did not manage to connect in time. Only call this after creating a client, otherwise the result will be invalid.
Arguments¶
None
Returns¶
True if the engine is operating in client mode and gave up to connect.
htme_clientDisconnect()
¶
Description¶
Disconnects from the server.
Arguments¶
None.
Returns¶
Nothing.
htme_clientIsConnected()
¶
Description¶
Check if the client is connected.
Arguments¶
None
Returns¶
True if the engine is running in client mode and if the client successfully connected to the server.
htme_disconnectNow()
¶
Description¶
This will close and disconnect a server or client. This will also clean the engine and you can go to the main room if the script return true. Use this when you want to exit the game and go back to the main menu.
if htme_disconnectNow()
{
room_goto(rm_main);
}
Arguments¶
None
Returns¶
True if ok to disconnect.
htme_findPlayerInstance()
¶
Description¶
Finds an instance created by a player. Only returns the first instance that was created by a player.
More information and examples: Players and Playerhashes
Arguments¶
Name | type | description |
---|---|---|
player | string | Hash identifier of the player |
Returns¶
An instance or -1 if no instance was found.
htme_getPlayerNumber(playerhash)
¶
Description¶
Extracts the number of the player from the playerhash The number starts at 1 for the server, 2 is the first connected client, 3 the third and so on. If 2 disconnects, the next player connecting will occupy place 2 again.
Arguments¶
Name | type | description |
---|---|---|
playe rhash | strin g | A playerhash as stored in the list that you can get with htme_getPla yers. |
Returns¶
The player number
htme_isLocal()
¶
Description¶
Returns true if the instance was created locally, false if it wasn’t or if it wasn’t even synced with the engine.
Arguments¶
None
Returns¶
True if calling instance was created locally and is synced, false otherwise.
htme_isLostConnection()
¶
Description¶
Check if the connection is lost. This will also clean the engine and you can go to the main room if the script return true.
if htme_isLostConnection()
{
room_goto(rm_main);
}
Arguments¶
None
Returns¶
True if the connection is lost.
htme_isServer()
¶
Description¶
Check if this engine is running in server mode.
NOTE: This will always return true when called by non local instances, this can’t be used to identify if a remote instance belongs to the server!
Arguments¶
None
Returns¶
True if running in server mode, except when called by a remote instance, in that case it will always return true.
htme_isStarted()
¶
Description¶
Check if a server or client was started
Arguments¶
None
Returns¶
True if the engine is started.
htme_serverDisconnect(player)
¶
Description¶
Asks a client to execute htme_clientDisconnect to disconnect. If he doesn’t within the timeout set in htme_config, he will be kicked by the server.
Arguments¶
Name | type | description |
---|---|---|
player | string | Hash identifier of the player |
Returns¶
Nothing.
htme_serverShutdown(player)
¶
Description¶
Shuts the server down and tells all player about it.
Arguments¶
None.
Returns¶
Nothing.
htme_syncGroupNow()
¶
Description¶
Force syncs all or specific group names one time in the next step. If you don’t provide any arguments, all groups will be force synced. Good if you have a group that syncs every 60 seconds but want to sync it earlier because you changed the value.
Arguments¶
None or up to 16 group names (string)
Returns¶
Nothing
Chat¶
htme_chatGetMessage(chat_queue_entry)
¶
Description¶
Get the message of a CHAT Interface message. See mp_chatGetQueue for details.
Example¶
Read the tutorial about creating a chat system for more information.
Arguments¶
Name | type | description |
---|---|---|
chat_queue_entry | string | An entry of the queue returned by mp_chatGetQueue. |
Returns¶
Message that was sent
htme_chatGetSender(chat_queue_entry)
¶
Description¶
Get the sender of a CHAT Interface message. See mp_chatGetQueue for details.
Example¶
Read the tutorial about creating a chat system for more information.
Arguments¶
Name | type | description |
---|---|---|
chat_queue_entry | string | An entry of the queue returned by mp_chatGetQueue. |
Returns¶
Playerhash of the player that sent this message
mp_chatGetQueue()
¶
Description¶
Object has to be set up with mp_syncAsChatHandler first, otherwise an error will occur.
Get a ds_queue containing all not processed string that recieved via this channel. All entries in this queue can be decoded using these commands: * htme_chatGetSender - To get the playerhash of the player that sent this message * htme_chatGetMessage - To get the chat message.
Example¶
Read the tutorial about creating a chat system for more information.
Arguments¶
None
Returns¶
A ds_queue, it’s entries can be decoded using the functions above.
mp_chatSend(message,[to])
¶
Description¶
Object has to be set up with mp_syncAsChatHandler first, otherwise an error will occur.
Send message via the CHAT Interface over this chanel. The message has to be a string. You can also experiment with json to send more complicated data.
This message will by default be sent to all clients and the server [also the local game!] and can be retrieved via mp_chatGetQueue.
Use the additional ‘to’ argument to only send this message to one player.
Example¶
Read the tutorial about creating a chat system for more information.
Arguments¶
Name | type | description |
---|---|---|
messa ge | strin g | The message to send |
[to] | strin g | (optional; Will send to all by default) hash of the player that the message should be sent to. |
Returns¶
Nothing
mp_syncAsChatHandler(channel)
¶
Description¶
Add this object as a handler for traffic via the CHAT Interface.
This object can then send and recieve traffic on the specified channel using mp_chatGetQueue and mp_chatSend.
This is independent from mp_sync and it’s mp commands. You don’t have to use mp_sync.
Example¶
Read the tutorial about creating a chat system for more information.
Arguments¶
Name | type | description |
---|---|---|
channel | string | The name of the channel to assign to this object |
Returns¶
Nothing
Online Lobby¶
udphp_downloadServerList(...)
¶
Description¶
Detailed information in `Tutorial - Bonus 1 - An ONLINE lobby <tutorial/13_lobby>`__.
Download the list of servers from the master server.
global.udphp_downloadlist_refreshing will be set true. It will be set false again if download is finished and global.udphp_downloadlist will contain a list of online servers then. If download fails, global.udphp_downloadlist_refreshing will never reset. It will fail, if the master server has the lobby disabled or is not reachable.
Use the (optional) arguments to sort and filter the list. Default sorting can be seen below under arguments.
Format of global.udphp_downloadlist:
ds_list:
[0...] => ds_map:
[ip] => string
[data1] => string
[data2] => string
[data3] => string
[data4] => string
[data5] => string
[data6] => string
[data7] => string
[data8] => string
[createdTime] => string -> can be converted to real
unix timestamp, time the server was created.
Arguments¶
Name | type | description |
---|---|---|
[limi t] | strin g/rea l | The number of servers to return or EMPTY STRING if ALL servers should be returned (optional) |
[sort by] | strin g | Field to sort the result by, this can be: date (filter by time created; DEFAULT), data1, data2, data3, data4, data5, data6, data7, data8 (optional) |
[sort by_d ir] | strin g | Sort ascending (“ASC”) or descending (“DESC”; DEFAULT) (optional) |
[filt er_d ata1] | strin g | Only list servers that match this excact string for their first data string (the game name). Can be EMPTY STRING if you don’t want to filter (optional) |
[filt er_d ata2] | strin g | See above; but for second data string (optional) |
[filt er_d ata3] | strin g | See above... (optional) |
[filt er_d ata4] | strin g | See above... (optional) |
[filt er_d ata5] | strin g | See above... (optional) |
[filt er_d ata6] | strin g | See above... (optional) |
[filt er_d ata7] | strin g | See above... (optional) |
[filt er_d ata8] | strin g | See above... (optional) |
Returns¶
Nothing
LAN Lobby¶
tme_startLANsearch(port,[gamefilter])
¶
Description¶
Detailed information in `Tutorial - Bonus 3 - A LAN lobby <tutorial/15_lanlobby>`__.
Search in the LAN for servers on the port specified. You can now use htme_getLANServers() to get a list of LAN servers. This list will be filled with servers over time. Run this comamnd again to empty it and resend the broadcast (to refresh the list).
Format of the list returned by **htme_getLANServers()**:
ds_list:
[0...] => ds_map:
[ip] => string
[port] => real
[data1] => string
[data2] => string
[data3] => string
[data4] => string
[data5] => string
[data6] => string
[data7] => string
[data8] => string
Arguments¶
Name | type | description |
---|---|---|
limit | real | The port to scan on |
[game filte r] | strin g | Only list servers that match this excact string for their first data string (the gamename). Can be EMPTY STRING if you don’t want to filter |
Returns¶
Nothing
Global Sync¶
htme_globalGet(name)
¶
Description¶
Returns a variable from the global sync list. See htme_globalSet for information on what these variables are. Will return undefined if variables was not stored previously by any client or the server. Make sure the player is connected or a server is running!
More information: Bonus 2 - Global Sync (Sync a pool of variables editable by all)
Example¶
var value = htme_globalGet("name")
if (is_undefined(value) {
//Nope, that wasn't set yet...
}
Arguments¶
Name | type | description |
---|---|---|
name | string | The name of the variable |
Returns¶
The saved variable (real or string) or undefined
htme_globalSet(name,value,datatype)
¶
Description¶
Stores a real or string value in the global sync list. Use buffer_type to define the type of the variable. The global sync list is a list of global variables that can be retrived via htme_globalGet.
They get synced between all clients and servers and be read and written by any (unlike instance variables which are read-only for all but the creator).
Make sure the player is connected or a server is running!
NOTE: Using this command will sync this variable immediately via SMART
More information: Bonus 2 - Global Sync (Sync a pool of variables editable by all)
Example¶
var value = 1+1;
htme_globalSet("name",value,buffer_u8);
Arguments¶
Name | type | description |
---|---|---|
name | string | The name of the variable |
value | real/string | The (new) value of the variable you want to store |
datatype | real | See Buffer type |
Returns¶
Nothing
htme_globalSetFast(name,value,datatype)
¶
Description¶
See htme_globalSet.
The only difference is that this script uses the FAST sync type. This means in rare cases when using this function the global Sync cache may be desynced, therefor only use this function in very specific cases.
More information: Bonus 2 - Global Sync (Sync a pool of variables editable by all)
Example¶
var value = 1+1;
htme_globalSetFast("name",value,buffer_u8);
Arguments¶
Name | type | description |
---|---|---|
name | string | The name of the variable |
value | real/string | The (new) value of the variable you want to store |
datatype | real | See Buffer type |
Returns¶
Nothing
Events¶
htme_error_message_handler(message)
¶
Description¶
You don’t call this script. You edit this script to show all the error messages from the engine. You can use the existing or create your own. The script will receive the error message as an argument.
Arguments¶
Name | type | description |
---|---|---|
message | string | The error message |
Returns¶
Nothing
htme_serverEventHandlerConnecting(script)
¶
Description¶
Call a script if a new player connected. This script has to meet the following specifications:
Callback Arguments:¶
Name | type | description |
---|---|---|
playe r_ma p | ds_m ap | a ds_map with the keys ip and port, which contain ip and port of the player |
Callback Returns:¶
Arguments¶
Name | type | description |
---|---|---|
scrip t | resso urce id of a scrip t | The script to call when a player connected. |
Returns¶
Nothing
htme_serverEventHandlerDisconnecting(script)
¶
Description¶
Call a script if a player disconnected. This script has to meet the following specifications:
Callback Arguments:¶
Name | type | description |
---|---|---|
player_map | ds_map | a ds_map with the keys ip and port and hash |
Callback Returns:¶
Nothing
Arguments¶
Name | type | description |
---|---|---|
scrip t | resso urce id of a scrip t | The script to call when a player disconnected . |
Returns¶
Nothing
Extending the Engine / Advanced Use¶
Interpolation¶
You will notice that sending keystrokes are not always optimal. Your player object will do some jittering when move. And you teleport from place to place sometimes. Here is a video to show why you need interpolation:
In short interpolation means that you travel from point 1 to point 2. But not by using keystrokes. Instead we use positions and interpolate (travel smootly) from point 1 to point 2. But it can be any 2 points. It can be one x position to another x position. It can be one image_angle to another image_angle. You can interpolate any values to make a smooth look.
To code this we use the demo project as template. In this example we only interpolate the x and y position. Start in the htme_obj_player create event:
mp_addPosition("Pos",600*room_speed);
We only need to update the position once we enter the room. The interpolation will fix the rest.
We are still in the create event and add this code after the mp_sync()
:
self.xpos=x;
self.ypos=y;
stepsToWaitUntilSendNextPos=6;
mp_add("interpolation","xpos,ypos",buffer_s16,stepsToWaitUntilSendNextPos);
This will sync the position to the other clients. We also need some variables to hold some values:
/// Interpolation setup
// use when check if new pos received
last_received_xpos=-1;
last_received_ypos=-1;
// interpolate values each step
travel_every_step_x=-1;
travel_every_step_y=-1;
// steps we interpolate
travel_every_step_counter=-1;
// save new pos if the other interpolation is not done yet
new_queue=ds_queue_create();
These will keep track of the last received position. So we know if we moved. And in interpolation we move some in each step so we use varialbes to hold the travel speed in. We also want to stack the positions if receive a new position before we are done.
Now brace yourself and add this in the Step begin event:
if (htme_isLocal()) {
// ====================
// Run on THIS computer (LOCAL)
// ====================
// This script here only run if the instance is local (our player on this computer)
// The engine will create our player on the other players screens
// So we now got our own player on this computer and the other players computers
// We must control what we want to send to our player on the other players computer
// Here you should add controls over your player
// if we got this inside if (htme_isLocal()) {
// This will only run on your computer and not on the other players computers
// We add some info to send to our player on the other computers
self.xpos=x;
self.ypos=y;
// We will set this on every step but the engine will only send it every
// mp_add("interpolation","xpos,ypos",buffer_s16,stepsToWaitUntilSendNextPos);
// ^
// 6
// Every 6 steps we send this info to our player on the other computers
} else {
// ==============================
// Run on OTHER players computers (NON LOCAL)
// ==============================
// This script here only run on the other players screens
// Here you should add what will happen on other players screens
// in the end step we use mp_map_syncOut() this is used to receive
// information from mp_map_syncIn()
// We now want to use the information we got from our player on our computer
// on this player computer
// So in the above we set
// self.xpos=x;
// self.ypos=y;
// The multiplayer engine sent it to this object on the other computer
// The engine also create a new instance of this object. And now the engine
// wait until you send information to it from your computer. Like you xpos and ypos
// So what should happen on the other players screen with our xpos and ypos
// we got from your computer?
// We want to interpolate.
// This will run every step but we dont get new positions every step do we
// So we must check when new information is received.
if self.xpos!=last_received_xpos or self.ypos!=last_received_ypos {
// We got a different xpos or ypos value from our computer
// Let us save this new pos in a queue
// We always save the new pos in a queue
// Because we might get a new pos before we are done with the first one
ds_queue_enqueue(new_queue,self.xpos,self.ypos);
// In a queue the values get out as they came in so if we add
// Its like puting cards one each other and draw them from below
// What comes in first draw first
// Ex you enqueue number 3,8,5,2,7,4 and then dequeue 4 of them
// you get 3 first then 8,5 and 2
// next time you dequeue you get 7 and 4
// Now lets save this pos as the last
last_received_xpos=self.xpos;
last_received_ypos=self.ypos;
}
// Now we check the queue if we got a new pos in it
if ds_queue_size(new_queue)>0 {
// First we check if we allready interpolating
// If we do then we must wait until it's done and then we can do the new pos
if travel_every_step_counter<1 {
show_debug_message("x:" + string(x) + " y:" + string(y))
// We now want to smootly move our player object on this player computer
// to that new x pos or y pos
// get the new x and y pos from the queue
// we saved x first and the y so
var newx=ds_queue_dequeue(new_queue);
// and then get y
var newy=ds_queue_dequeue(new_queue);
show_debug_message("new x:" + string(newx) + " new y:" + string(newy))
// Ok now we must move it some pixels at a time every step
// And we know how long it took to travel this new pos
// We sent this new info every 6 step so from x to new x it took 3 steps
// Now we check if we got many new pos saved
// If we got to many our player on the other computer will
// fall back some and desync
// So we check if we got 3 new pos then we can just skip one and travel
// to the next
if ds_queue_size(new_queue)>2 {
// We got to many new pos lets take one more out
var newx=ds_queue_dequeue(new_queue);
var newy=ds_queue_dequeue(new_queue);
// But now we took 2 new values
// And we know it took 6 steps to travel to one new pos
// so if we take out 2 new pos it must have taken 12 steps
// to travel there. But to avoid that we fall back again
// lets only do it in 11 steps
// We set the stepsToWaitUntilSendNextPos in the create event
var StepsWeTravel=(stepsToWaitUntilSendNextPos*2)-1;
} else {
// Set time it took to travel to this new pos
// We only took one new pos so it took 6 steps to travel there
var StepsWeTravel=stepsToWaitUntilSendNextPos;
}
// First we check the distance from current pos on this computer (x,y)
// To your new position we sent to this copmputer (xpos, ypos)
var distance_to_move_x=newx-x;
var distance_to_move_y=newy-y;
// Then we calc how much we must move to reach the new pos in the steps we want
travel_every_step_x=distance_to_move_x/StepsWeTravel;
travel_every_step_y=distance_to_move_y/StepsWeTravel;
// We want to only move our player on other computer in 6 steps (or more if we took 2 new pos) so lets count it
travel_every_step_counter=StepsWeTravel;
}
}
// We now must handle the movement
// First we check if we got any counts left
if travel_every_step_counter>0 {
// Ok we must travel some
// Let add it to x and y on this computer
// Remember this script will never run on your computer where you control the player
// Only on the other players computers
show_debug_message("travel x:" + string(travel_every_step_x) + " travel y:" + string(travel_every_step_y));
x+=travel_every_step_x;
y+=travel_every_step_y;
show_debug_message("new x:" + string(x) + " new y:" + string(y));
// Now we remove one count
travel_every_step_counter-=1; // same as travel_every_step_counter=travel_every_step_counter-1;
show_debug_message("steps left:" + string(travel_every_step_counter))
// When it reach 0 we will check if we got a new pos we can interpolate to
}
}
// COMMON
// This will run on this computer and the other computers
// The mp_map_syncIn only run on the local. If we are on the remote it will just ignore this
mp_map_syncIn("xpos",self.xpos);
mp_map_syncIn("ypos",self.ypos);
I hope you got all.
The last one is in the step end event:
self.xpos = mp_map_syncOut("xpos", self.xpos);
self.ypos = mp_map_syncOut("ypos", self.ypos);
This will now interpolate the position and make your player move smootly from one position to the next. You can also download a demo here: https://drive.google.com/file/d/0BxE4k4xEiNO2azAzNkZObUV0NnM/view?usp=sharing
Troubleshooting¶
Check if client is connected¶
Sometimes you want to check if another player is conencted.
Use htme_getPlayers()¶
htme_getPlayers()
will return a ds list with all the connected players.
You can check if above 1 and then you know a client is connected.
var playerlist = htme_getPlayers();
var total_players=ds_list_size(playerlist);
if total_players>1 {
show_debug_message("Another player is connected!");
}
Why do i control two instances¶
When you start two instances of your game you may experience that you control P1 and P2. This may be because of missing htme_isLocal or windows may send the controls to both games at the same time. Here are some thins you can try.
Use htme_isLocal()¶
You must make sure that all your keyboard and mouse inputs are inside a htme_isLocal()
statement like this:
if (htme_isLocal()) {
self.pressed_jump = keyboard_check(vk_space);
self.pressed_left = keyboard_check(vk_left);
self.pressed_right = keyboard_check(vk_right);
}
This will make sure that only the local player run the control code.
Use Dual window extension¶
With this extension game maker will start 2 games when you test them in game maker. When you click one window the other may not receive the controls. Import this extension: https://drive.google.com/file/d/0BxE4k4xEiNO2dEY5bUZ0dmo1b1E/view?usp=sharing And add this object in the first room: https://drive.google.com/file/d/0BxE4k4xEiNO2NW81Q1FnOTdsUUU/view?usp=sharing
Use Windows exe and game maker player export¶
You can also try export the game to windows exe and then run the game in game maker with game maker player export selected. This may stop windows to send the input to both games at the same time.
Error on connect¶
Sometimes you may experience errors when you try connect. Here are some error messages and the solutions.
Port is a real¶
ERROR in
action number 1
of Step Event0
for object obj_htme:
Illegal argument type
at gml_Script_htme_clientConnect (line 29) - network_send_udp( self.socketOrServer, self.server_ip, self.server_port, self.buffer, buffer_tell(self.buffer) );
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Script_htme_clientConnect (line 29)
called from - gml_Script_htme_step (line 59) - htme_clientConnect();
called from - gml_Object_obj_htme_StepNormalEvent_1 (line 2) - htme_step();
Make sure you provide a real and not a string as port argument in htme_clientStart
.
htme_clientStart(ip,real(port))
Fire input not received on client¶
Sometimes you may experience that some keytrokes is missing on the client side. Ex say you use this code:
Create event:
self.firenow=false;
mp_add("firebutton","firenow",buffer_bool,1);
Step Begin event:
if (htme_isLocal())
{
self.firenow= keyboard_check_pressed(vk_space);
}
mp_map_syncIn("firenow",self.firenow);
Step End event:
self.firenow=mp_map_syncOut("firenow",self.firenow);
This code send a network message to the other players every step. But sometime the client wont receive the message. This is because of network lag.
You send 30 network messages per sec. These messages is sent to the client.
But every message take about 10-600 milliseconds to reach the other player.
This means that some of the messages may be bundled and be received at the same time.
Say you press fire 2 times within 1 sec. Under 1 sec the messages look like this.
Every 0 is a no fire and 1 is a fire. Ever space is a step in gm. You send a perfect array of messages.
0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0
But the player who receives the messages may receive them like this
000 0000001
00 0000 000100
00000000
The other player received several messages in the same step. 000 is ok.
0000001 is ok because the 1 was the last message received in this step and self.firenow
will be true when the step event run in the player.
00 and 0000 ok. But here it comes. 000100 here the other player receive 6 messages in the same step.
Even if 1 fire was in there the last message received set self.firenow
to false. So no fire this step.
Send only when needed¶
You can fix this by increasing the mp_add("firebutton","firenow",buffer_bool,room_speed*100);
This will stop to send messages every step. But when you fire you need to send it.
You can do this by calling htme_syncGroupNow("firebutton")
.
if htme_isLocal() {
If keyboard_check_pressed(ord(“f”)) {
firenow=true mp_map_syncIn(“firenow”,self.firenow); htme_syncGroupNow(“firebutton”);} Else if keyboard_check_released(ord(“f”)) {
firenow=false mp_map_syncIn(“firenow”,self.firenow); htme_syncGroupNow(“firebutton”);}
}
Player numbers¶
It can be tricky to get the player number. But here is how to do it.
Get current player¶
Use this code to get the local player number. If you are the server your number is 1. If another player connects he is number 2. If you have 3 players and p2 disconnect nr 2 is free. When someone joins he will get the free nr 2.
htme_getPlayerNumber(global.htme_object.playerhash);
Get remote player number from object¶
You can also get the player number from any synced object. Use this code to check what player a synced object belongs to.
htme_getPlayerNumber(obj_synced_player.htme_mp_player);
How to quit/disconnect¶
When you start a server or connect a client to a server and want to quit while you are in the game.
Use htme_disconnectNow()¶
After you call htme_disconnectNow()
the gmnet engine will disconnect and you can go to the room you want.
Make sure you remove all other network related objects after.
if htme_disconnectNow() {
// If disconnect is ok then do some cleaning
// Remove persistent but not synced objects
with htme_obj_chat instance_destroy();
with htme_obj_playerlist instance_destroy();
// Go back to menu room
room_goto(htme_rom_menu);
}
How to update¶
When updating, check the changelog and this page to see which files have changed. When you made changes to these files, make a backup and update the asset.
After you are done updating, check what has changed (you might find detailed information in the changelog) and merge the new version with your changes.
Recent updates¶
1.2.2 - CHAT Interface, Event Handlers, RPCs, MIT License¶
- LICENSE CHANGED TO MIT Due to confusion about the GPLv3 license, I decided to change the license to the MIT license. This is way easier to understand for everybody. BY UPDATING YOU AGREE TO THIS NEW LICENSE. THIS LICENSE REPLACES THE OLD ONE.
- Support for custom Event Handler for Connection/Disconnection on Server Also adds an easy way to reject players when they conect
- Fixed a bug that prevented the game to be run with the YYC (thanks to Imtnt @gmc)
- Server Shutdown Function added and automatic shutdown/disconnect if the htme object is destroyed / game is ended
- PLUS/UDPHP : UDPHP 1.2.1 - Fixed an issue where servers and client could not connect to each other when the master server was not reachable.
- Added CHAT Interface: This is a new set of functions that allow you to send and recieve string messages in a more clasic way. The manual was updated so the Chat Tutorial now uses this new way of syncing (an RPC tutorial was also added to the manual; see below).
- Updated debug overlay to include debug information about the CHAT Interface
- Fixed a bug, where in rare cases clients could start syncing before they got their player hash which lead to invalid instances being synced and mayor desync
- MANUAL (http://htme.parakoopa.de/manual) :
- Added manual pages for the event handler scripts
- Added Tutorial Chapter Bonus 4 - Event Handlers for Connecting/Disconnecting
- Added manual page for htme_shutdown
- Added manual page for htme_globalSyncFAST
- Added manual page for the CHAT Interface and all it’s functions
- Rewrote Tutorial 11 - Chat (it is now using the CHAT Interface)
- Added Tutorial Chapter Bonus 5 - RPC
- Updated debug overlay manual
- Master Server is now on Version 1.2.3, you might want to update, some critical bugfixes were made.
- **We now have a forum: http://htme.parakoopa.de/forum - Check it out - This is now the main place for support! **
1.2.1 - Introducing a better online lobby and a LAN lobby for all!¶
This update improves the ONLINE lobby by adding fltering and sorting mechanisms. Check out the updated tutorial! Also we added a seperate lobby for LAN Servers, the master server was HUGELY updated and we are happy to introduce a testing tool for master servers
- Added a LAN lobby.
- UDPHP/PLUS: Added a new lobby system that supports filtering
- UDPHP/PLUS: Added support for new registration and sending version number
- UDPHP/PLUS: Changed it so the server only reconnects to the master server if it lost connection
- You need to update the master/mediation server if you are a GMnet ENGINE!!
Master server changelog¶
Master server Version 1.2.0
- Added the possibility to name the server
- Added –version parameter
- Added –testing paramter to be tested via HTMT.
- Added multiple testing commands
- Added a filter for a minimum required udphp version
- Changed registration so it requires the udphp version of the server that wants to register
- Fixed bugs in server destruction
- Added a createdTime field to the servers
- Added filter and sorting methods to the lobby
CHANGED FILES IN 1.2.1:
scripts/htme_init.gml
objects/obj_udphphtme_lobby.object.gmx
scripts/udphp_config.gml
scripts/udphp_downloadServerList.gml
scripts/udphp_serverCommitData.gml
scripts/udphp_Punch.gml
scripts/htme_getDataServer.gml
scripts/htme_getLANServers.gml
rooms/htme_lanlobby.room.gmx
scripts/htme_networking_searchForBroadcasts.gml
objects/htme_obj_lanlobbydemo.object.gmx
objects/htme_obj_menu.object.gmx
scripts/htme_serverBroadcast.gml
scripts/htme_serverStart.gml
scripts/htme_startLANsearch.gml
scripts/htme_stopLANsearch.gml
scripts/htme_step.gml
1.2.0 - Debug overlay and graceful discon¶
I recommend to reinstall GMnet ENGINE, meaning removing the old asset and adding the new one. Make sure to backup your config.
- Added htme_serverDisconnect and htme_clientDisconnect to gracefully kick players or disconnect from the server.
- Added a debug overlay. All information about that here.
- Changed visuals of demo project
- UDPHP (PLUS only): Fixed bug that resulted in not closing old TCP connections on the servr when reconnecting.
- Added globalSetFAST. Same as globalSet but with the fast sync type.
CHANGED FILES IN 1.2.0:
objects/htme_obj_playerlist.object.gmx
objects/obj_htme.object.gmx
rooms/htme_rom_demo.room.gmx
rooms/htme_rom_demo2.room.gmx
scripts/htme_clientDisconnect.gml
scripts/htme_clientNetworking.gml
scripts/htme_clientShutdown.gml
scripts/htme_debugOverlayEnabled.gml
scripts/htme_debugoverlay.gml
scripts/htme_doDrawInstanceTable.gml
scripts/htme_doGlobalSync.gml
scripts/htme_doInstAll.gml
scripts/htme_doInstCached.gml
scripts/htme_doInstInvisible.gml
scripts/htme_doInstVisible.gml
scripts/htme_doMain.gml
scripts/htme_doMain_new.gml
scripts/htme_doOff.gml
scripts/htme_doPlayers.gml
scripts/htme_doSignedPackets.gml
scripts/htme_doStateInstAll.gml
scripts/htme_doStateInstCached.gml
scripts/htme_doStateInstInvisible.gml
scripts/htme_doStateInstVisible.gml
scripts/htme_doStateMain.gml
scripts/htme_doStateOff.gml
scripts/htme_do_createMicro.gml
scripts/htme_dotbd.gml
scripts/htme_findPlayerInstance.gml
scripts/htme_globalSetFast.gml
scripts/htme_globalSet_new.gml
scripts/htme_init.gml
scripts/htme_sendGSFast.gml
scripts/htme_sendGS_new.gml
scripts/htme_serverDisconnect.gml
scripts/htme_serverKickClient.gml
scripts/htme_serverNetworking.gml
scripts/htme_serverProcessKicks.gml
scripts/htme_serverStart.gml
scripts/htme_step.gml
scripts/mp_add.gml
scripts/udphp_serverPunch.gml
sprites/htme_spr_door.sprite.gmx
sprites/htme_spr_player.sprite.gmx
sprites/images/htme_spr_door_0.png
sprites/images/htme_spr_player_0.png
sprites/images/htme_spr_wall_0.png
1.1.0 - Introducing: Global Sync¶
- Added Global Sync to sync global variables that are read- and writeable at any time by all clients and the server.
CHANGED FILES IN 1.1.0:
htme_clientNetworking.gml
htme_clientStart.gml
htme_init.gml
htme_serverEventPlayerConnected.gml
htme_serverNetworking.gml
htme_serverStart.gml
htme_recieveGS.gml (added)
htme_sendGS.gml (added)
htme_globalGet.gml (added)
htme_globalSet.gml (added)
1.0.0 - The lobby update¶
0.6.0 - The performance update¶
The great performance update! - ADDED A NEW LICENSE. BY UPDATING YOU AGREE TO THIS NEW LICENSE. – The license is still GPLv3, but it comes with an additional permission to create games in Game Maker without having you to provide the source code of your game when using the Mutliplayer Engine - Made isLocal faster - Improved behaviour and performance of signed packets. Reliable data will no longer by sent if new data is available - Replaced most of the maps with lists, resulting in a huge performance boost! - Fixed crashes and desyncs on certain events - Fixed a mayor memory leak and improved memory managment - COMES WITH udpph 1.1.2. - No mayor changes to the local manual! - Online manual was updated with changelog.
CHANGED FILES IN 0.6.0:
htme_cleanUpInstance.gml
htme_clientBroadcastUnsync.gml
htme_clientStart.gml
htme_createSignedPacket.gml
htme_createSingleSignedPacket.gml
htme_forceSyncLocalInstances.gml
htme_init.gml
htme_isLocal.gml
htme_recieveSignedPackets.gml
htme_recieveVarGroup.gml
htme_removeSignedPacket.gml
htme_removeSignedPacketsByCatFilter.gml
htme_roomstart.gml
htme_sendSignedPacket.gml
htme_sendSignedPackets.gml
htme_serverBroadcastUnsync.gml
htme_serverCreateSPForAllCheckRoom.gml
htme_serverEventPlayerConnected.gml
htme_serverEventPlayerDisconnected.gml
htme_serverKickClient.gml
htme_serverNetworking.gml
htme_serverRecreateInstancesLocal.gml
htme_serverRemoveBackup.gml
htme_serverSendAllInstances.gml
htme_serverStart.gml
htme_serverSyncPlayersUDPHP.gml
htme_syncInstances.gml
htme_syncSingleVarGroup.gml
mp_add.gml
mp_unsync.gml
udphp_stopServer.gml
udphp CHANGELOG:
udphp 1.1.2: * Fixed stopServer not working anymore
udphp 1.1.1: * Fixed some bugs that were created with the changes in 1.1.0. ...
0.5.0¶
Initial release
Getting Started¶
Always wanted to make a multiplayer game? But you don’t want to host your own dedicated servers or want the players to open ports in their firewall? Do you just want them to simply press a button to play wih their friends?
GMnet PUNCH allows you to do the impossible! With simply calling a few scripts in your game you can get your game to the next level of multiplayer experience.
What is UDP hole punching?¶
UDP hole punching is a technique that allows UDP packets to be send, that normally a NAT would block. Normally if you want to host a game, you have to open the ports, because otherwise your NAT would block the UDP (or TCP) packets that are required for network communication. UDP hole punching works because in the process the client does not simply “connect” to the server. The server also sends packets back, which means both try to connect at the same time to each other. Then both NATs will accept the connection.
The problem is that at least the server doesn’t know when a client connects to it. That’s why we need a (globally reachable= master server. The server registers at the master server and opens a TCP connection so the master server can send the client informations*. When a client tries to connect, it also registers it’s UDP port at the master server and tells it to which server it wants to connect. If the server is found, the ports will be send backd via TCP. The client get’s the server port/ip and the server the client port/ip. Then both send the packets and the hole is punched. Both players are connected to each other.
(* TCP allows the master server to send packets back to the server. That’s because TCP works with connections and if the server opens a connection to the master server, both can send packets to each other, even if the server is behind a NAT.)
Requirements¶
Besides a valid Game Maker: Studio license you will also need to host a master server somewhere. “Somewhere” means it has to be reachable from the internet! [Ports must be opened] Also please note that this only works with UDP game servers! - Setting up UDP game servers in Game Maker is very differnt from TCP game servers since UDP is connectionless. If you need help, contact me!
Wait, I need to host a “master server”?! - Where do I get that?¶
The server GMnet GATE.PUNCH that is needed to connect clients and servers is free software and the source code can be found on it’s product page.
It is written in java and you can either download the binarys or the well-documented source code. Adjust the mediation server to fit your needs or write your own (you can find the communication “protocol” below) It comes with everything that is needed for TCP and UDP communication with Game Maker: Studio and can also be used to create a dedicated multiplayer server or something elese.
You need to host this server yourself. If you can’t right now, contact me, I can host a server for you for testing purposes. For the demo you can also connect to the default IP and port (please note that this demo server might be down).
If you need more help setting up the server, check out the tutorial page of GMnet ENGINE on this topic.
UPnP¶
UPnP stands for Universal Plug and Play.
GMnet PUNCH use UDP hole punching to make the NAT/router allow online connections.
UPnP automates the port forwarding on the users router or NAT. It sends a command to the router to forward a port.
But not all NATs allow UDP punch. The user must then manually port forward the server port (ex 6510) or use other methods such as UDP Hole-Punching (as done by GMnet PUNCH) that can also allow online play without UPnP or port forwarding.
By enabling UPnP you allow users who don’t forward their port and that are in networks that don’t support UDP Hole-Punching to play anyway.
When your users start the server, UPnP will try to port forward the server port for them.
You can enable UPnP in udphp_config
.
Note
UPnP won’t work 100% for everybody (even if UPnP is supported) when used with the GMNet PUNCH master server until version 1.4.0!
But there is no harm in turning it on anyway.
Features¶
- Multiplayer without port forwarding - Your players can enjoy gaming with their friends without having to deal with anoying firewalls
- Builtin server and client structure that simply connects with the one you already have - Just follow the instructions and add it to your game.
- Easily extendable to support Peer-to-Peer gaming!
- Open source (GPLv3) - You have the source code, you can modify it in any way you want and also use it as much as you want
- Simple to use with tutorials and sample project - You download the sample project with all required GMnet PUNCH scripts. You can just press play and try it out.
- Well documented - Not only do you get a sample project! GMnet PUNCH is well documented and easy to read. And since it’s open source and you bought it, you can easily change it to your needs.
- Easy to use - For both you and the players!
- Using a master/mediation/rendevouz server… - You don’t need to host a whole game server, you only need a small lightweight server that connects the clients to your server. More information can be found on the page Requirements.
- Fallback mode when UDP hole punching doesn’t work - In local networks GMnet PUNCH will automatically try to connect directly to the server.
- Infinite clients - Supports infinite clients in a single game.
Tutorial¶
Implementing GMnet PUNCH in your game¶
To implement GMnet PUNCH in your game copy all scripts and the behaviour of all the objects to your game and adjust them to your needs. When copying the behaviour, you can simply use your existing server/client objects and add the codes at the top of your events.
obj_control¶
Let’s actually talk about this “behaviour”: I will not go over the debug objects in the sample project, I will only guide you through the neccassry objects/code. Let’s start with object obj_control. The only important thing in obj_control is the create event. In the create event you can find, that udphp_config is called. This is the first you have to call when you want to use GMnet PUNCH.
How to use udphp_config (arguments):
- master_ip(stringl) -> The IP of the master server that both server and client will use to connect to each other. Confused? See the section Wait, I need to host a “master server”?! - Where do I get that?.
- master_port(real) -> The UDP and TCP port the master server listens on.
- reconnect intverval(real) -> After this amount of steps the server will reconnect to the master server. This has to be done, to ensure the server is always connected to the master server. Future versions will auto-reconnect on loss of connection, but we are still testing that out.
- connection timeout(real) -> After this amount of time the server and client will give up to connect t eachother. The client will display a connection failed message. This timeout will also be used if you set the client to directly connect to the server.
- debug(boolean [0/false or 1/true]) -> Show more debug messages. Not recommended for production use.
- silent(boolean) -> This will turn ALL debug messages off. Recommended for production use.
Copy the call of this script to your game startup or somewhere else where it get’s called before the other GMnet PUNCH scripts. Make sure no global variables starting with udphp_ are used. These are reserved for GMnet PUNCH.
Server¶
Okay so we are done in obj_control. Let’s go to obj_server and obj_client. Both have the same structure. GMnet PUNCH uses the create event, the step event and the networking event of both. The following scripts can jsut be inserted as [the first] entries in these events. The GMnet PUNCH scripts will only run when needed and they won’t interupt your networking. If they do, please contact the support!
Server create event¶
obj_server’s create event get’s called on server creation (obviously). You need to create a player list (ds_list) there. This list will be used by GMnet PUNCH to store all players in. How to use them in your game will be explained later, but this is your only way to tell which player communicates with the server! You will also need a buffer that will be used for communication by the server. It should either be large or a growing buffer. The last thing you’ll need is a UDP server created by network_create_server(network_socket_udp,{port},{maxplayers}. All these will be fed into the udphp_createServer script.
*How to use udphp_createServer (arguments)*:
- server - A UDP server
- buffer - A buffer
- player-list - A ds_list that will store all players (more information below)
udphp_createServer will return either true or false. If the returned value is false, the server creation failed.
player_list = ds_list_create();
buffer = buffer_create(256, buffer_grow, 1);
server = network_create_server(network_socket_udp,1234,32);
ret = udphp_createServer(server,buffer,player_list);
if (!ret) {
//Server could not be created. Destroy instace. GMnet PUNCH will also show a message if not silent.
instance_destroy();
}
Server step event¶
In obj_server’s step event the actual magic happens. Just run udphp_serverPunch with no arguments.
Server networking event¶
And in obj_server’s networking event we share the magic: Simply run udphp_serverNetworking with no arguemnts.
Server: Client identification¶
To identify clients in the networking event, you take the ip and the portof async_load and check if they are in the player list you created earlier. The player list stores keys in the format “ip:port”.
To see if a player is in the player list:¶
var in_ip = ds_map_find_value(async_load, "ip");
var in_port = ds_map_find_value(async_load, "port");
var exists = ds_list_find_index(player_list, in_ip+":"+string(in_port));
//exists will give you the position of the player in the list or -1 if he's not in there
To get ip and port of a player entry:¶
var entry = /* Entry from the player list */
var ip = udphp_playerListIP(entry);
var port = udphp_playerListPort(entry);
To send a message to all players iterate over the player list and send a packet to all players (see hello world demo in step event).
Client¶
Client create event¶
To create the udphp client with udphp_createClient you need a UDP socket (not server!), a buffer, as well as the server ip. The rest is explained in the argument listing: *How to use udphp_createClient (arguments)*:
- UDP client socket - A UDP socket
- Server IP(string) - The IP of the server to connect to (not the master server ip!)
- buffer - A buffer
- directconnect(bool) - If you set this to true, GMnet PUNCH will be bypassed. The client willdirectly connect to the server without hole punching. directconnect is automatically set to true when the master server is down or the server is not registered at the master server
- server_port(real) - This is optional. Set to 0 when unkown. This will be used (and is then required for successful connection) when the client connects directly and ONLY then. See above to know when it connects directly.
IMPORTANT: The function will return the client id you have to store. This id is used to identify this client. Using this you can run multiple clients in one game (as used in the demo). It will return -1 if the client could not be created.
buffer = buffer_create(256, buffer_grow, 1);
client = network_create_socket(network_socket_udp);
client_id = udphp_createClient(client,server_ip,buffer,false,1234);
if (client_id < 0) {
//Client could not be created. Destroy instace. GMnet PUNCH will also show a message if not silent.
instance_destroy();
}
`
Client step event¶
In the step event simply run udphp_clientPunch with the stored client id as argument. This function will return false if the client’s connection failed. This is the only way to determine if a connection process failed and it will return true in all other cases.
if (!udphp_clientPunch(client_id)) {
//When this returns false, the connection failed or the client was destroyed.
show_message("Connection failed!");
instance_destroy();
}
`
Client networking event¶
In the networking event simply call udphp_clientNetworking with the client id as an argument.
Client: Get IP anf Port of server / check if connected¶
To communicate with the server after you are connected you of course need IP and port of the server, you also need to know if you are already connected. To do that use these functions:
var connected = udphp_clientIsConnected(client_id); //true or false
if (connected) {
//The next functions will return invalid information if you use them without making sure you are connected:
var ip = udphp_clientGetServerIP(client_id); //String
var port = udphp_clientGetServerPort(client_id); //real
}
Please see the hello world demo scripts in obj_client for an example.
GMnet PUNCH is now set up!¶
In the next page we will show the last feature: The online lobby!
Lobby¶
GMnet PUNCH features a lobby system. Let me guide you through it.
First things first¶
Server, client and obj_control are now persistent, so they exist in the lobby room as well. There is also a new object obj_lobbyclient, which is explained later.
Testing the lobby¶
When you start a client in the demo project and click yes when asked you will be brought to the demo lobby room. This is a very simple demo room that can only show four servers. Once we are done, you’ll be able to create an even better lobby.
To test it, start a server and see if it appears in the lobby. If you use the demo master server, please note that there might be “GMnet ENGINE Demo Servers” in the server list. You can not join them, they were created using the GMnet ENGINE demo project. Both use the same lobby and the same demo master server.
Data strings¶
There are 8 strings that can be used to identify your game in the lobby.
Open the create event of obj_server and you’ll see this addition to the server creation code:
udphp_serverSetData(1,"udphp_demo113"); //1 is reserved for game name and version! Change it when making your own game!
udphp_serverSetData(2,"GMnet PUNCH demo Server"); //2 is used for game name in our demo lobby
udphp_serverSetData(3,"This is a GMnet PUNCH demo server!"); //3 is used for game description in our demo lobby
These 3 strings are used by the demo project for (1) an identifier for the demo project, (2) the name of the server, (3) a description of the server. You can use all 8 for whatever you want, but (1) as a game identifier is my recommendation.
We set the data strings using udphp_serverSetData(n,string)
right
after the server was created. They are stored in the variables
global.udphp_server_data1-8
.
If you want to update any data string later, after the server connected
to the master server, you need to use udphp_serverCommitData()
. This
syncs all data strings to the master server. You MUST NOT use it here,
because the server hasn’t connected to the master server yet!
Building the lobby¶
The room of the lobby is the room udphhtme_lobby
. This room only
contains the object obj_udphphtme_lobby
. This object controls the
lobby. Let’s dive into it!
The create event¶
//IF YOU USE GMnet PUNCH - it will only let you connect to GMnet PUNCH servers:
if (!script_exists(asset_get_index("htme_init"))) {
self.game = "udphp_demo120"
}
//IF YOU USE GMnet ENGINE - it will only let you conect to GMnet ENGINE servers
else {
self.game = "htme_demo121"
}
//IF YOU USE YOUR OWN SERVER - Change self.game!
///Recieve lobby data from the master server
udphp_downloadServerList(4,"date","DESC",self.game);
First the variable game
gets set. We use this to prevent GMnet
ENGINE players from joining GMnet PUNCH servers and vice-versa. As said
before, this object is used in GMnet ENGINE demo project and GMnet PUNCH
standalone demo project, this is why it has seperate code for both.
The important part here is udphp_downloadServerList
. This will tell
GMnet PUNCH to download a list of servers from the master server. The
paramters allow for filtering and sorting the result, we use this to
only get results that match our game name we store in datastring 1 in
this example. More information about what you can filter, can be found
on the usage page of udphp_downloadServerList in the GMnet ENGINE
manual.
You use your own filtering variables later when creating your lobby.
The networking event¶
///Waits for master server response
udphp_downloadNetworking();
This code checks if the master server sent the server list and updates it.
The draw event¶
The draw event is split up into different sub-scripts:
‘Background’, ‘Title and Controls’, ‘Online servers’¶
Draws some background colors and some text, not important
‘Servers (Loop)’¶
This draws the actual server list.
Let’s analyze it:
///Servers (Loop)
var l = global.udphp_downloadlist;
for (var i = 0; i<4;i++) {
draw_text(10,85+80*i,"=("+string(i+1)+")=");
First, the list global.udphp_downloadlist
is stored in the local
variable l
(because it’s shorter). This ds_list contains all the
servers we got from the master server.
Then it begins a loop that loops through the first 4 servers in the list we got by the master server, everything is in this loop and then it draws a nice little number for each server.
if (ds_exists(l,ds_type_list)) {
if (ds_list_size(l)>i) {
Now, this is the interesting part.
First we check if the downloadlist was already created (it get’s created
once the list has been downloaded). After that we check if it has at
least as many entrys as the server we want to list. For this example we
assume i
is 1. That means it checks if there is atleast one server
in the list. If yes, we have an entry we can now draw.
//Get stuff from the downloadlist
var entry = l[| i];
var ip = entry[? "ip"];
var game = entry[? "data1"];
var servername = entry[? "data2"];
var description = entry[? "data3"];
Now the entry (a ds_map) for our server is extracted from the list and we get the gamename, which is stored in data1, the ip, which is stored in the key “ip”, the name of the server, which we stored in data2, and so on.
draw_text(70,85+80*i,servername+" | "+ip);
draw_text(70,115+80*i,description);
}
}
draw_line(0,160+80*i,room_width,160+80*i);
}
Now we just draw everything.
The press 1-4 key events¶
Pressing 1-4 on the keyboard will connect to that game. Let’s see how!
///LOAD GAME SERVER ON SLOT 1
var l = global.udphp_downloadlist;
if (ds_exists(l,ds_type_list)) {
if (ds_list_size(l)>0) {
var entry = l[| 0];
var ip = entry[? "ip"];
var game = entry[? "data1"];
We again open the downloadlist and check if server 1 is in it, if yes we continue.
if (game != self.game) {
//Not compatible game, exit
show_message("Game server or version is incompatible!");
exit;
}
Remember the filtering variable we created in the create-event? We use it here to check if the server is a GMnet PUNCH demo game. If not we cancel. Please note, that this is propably not needed here, since we filtered out all, but our game in the create event, when we ran udphp_downloadServerList.
//====UDPHP DEMO ONLY
if (!script_exists(asset_get_index("htme_init"))) {
//Create new client - See obj_client + manual for more information
global.tmp_lobby_ip = ip;
instance_create(0,0,asset_get_index("obj_lobbyclient"));
//Return to main room
room_goto(asset_get_index("udphp_room"));
}
//====GMnet ENGINE DEMO ONLY
else {
//This code is irrelevant for UDPHP and has been removed
}
} else {
//Do nothing - There is no server on this slot
}
} else {
//Do nothing - There is no server on this slot
}
This is the rest of the script. We once again check if we are running the GMnet ENGINE demo project and then we begin connection.
We create a new instance of obj_lobbyclient, a object that is child of
the object obj_client.This means it has the same events. We only
changed the create event to get the ip from global.tmp_lobby_ip
rather than asking for it. It will create a new client same way
obj_client
does and control it.
Done!
And this is how you create a lobby! Now go ahead and do it! :)
How can my clients get the gamename and data strings after they connected?¶
Please see script udphp_clientReadData
for how this works.
Function reference¶
These are very brief and incomplete information. Detailed information can be found in the script files
The following scripts are scripts YOU as the game developer use:
Setup:¶
- udphp_config One-time setup script Usage: udphp_config(master_ip,master_port,reconnect_intv,timeouts,debug,silent,delta time,upnp)
Server:¶
- udphp_createServer To be used in the Create event of the server Usage: udphp_createServer(udp_server,buffer,player_list,upnp port)
- udphp_serverPunch To be used in the step event of the server Usage: udphp_serverPunch()
- udphp_serverNetworking To be used in the networking event of the server Usage: udphp_serverNetworking()
- udphp_stopServer Usage: udphp_stopServer() Delete the instance afterwards
Client:¶
- udphp_createClient To be used in the Create event of the client Note: Returns client id Usage: udphp_createClient(udp_socket,server_ip,buffer,directconnect,directconnect_port)
- udphp_clientPunch To be used in the step event of the client Usage: udphp_clientPunch(id)
- udphp_clientNetworking To be used in the networking event of the client Usage: udphp_clientNetworking(id)
- udphp_stopClient Usage: udphp_stopClient(client_id) Instance should be deleted with the false return value of clientPunch
Tools:¶
- udphp_clientGetServerIP [for client] This will return the server ip of this client and should only be used if the client is connected. Usage: udphp_clientGetServerIP(client_id)
- udphp_clientGetServePort [for client] This will return the server port of this client and should only be used if the client is connected. Usage: udphp_clientGetServerPort(client_id)
- udphp_playerListIP [for server] Get an ip out of a player list entry (see serverCreate for details) Usage: udphp_playerListIP(player)
- udphp_playerListPort [for server] Get an port out of a player list entry (see serverCreate for details) Usage: udphp_playerListPort(player)
- udphp_clientIsConnected [for client] With this you can check if your client has conncted to the server. Usage: udphp_clientIsConnected(client_id)
Lobby:¶
- udphp_downloadServerList [no server or client has to be running] Download a list of servers from the lobby. More information. Usage: See GMnet ENGINE manual page
GMnet PUNCH/GATE.PUNCH “protocol” (for building your own server)¶
This is a linear breakdown of what packets are sent:
- WHEN FOUND: -> Via the client’s TCP socket: Send UDP ip&port of server to client [packet: buffer_s8 (-1), buffer_string (ip), buffer_string (port)] -> Via the server’s TCP socket: Send UDP ip&port of the client to server [packet: buffer_s8 (-1), buffer_string (ip), buffer_string (port)]
- WHEN NOT FOUND: -> Send not found packet to client via TCP [packet: buffer_s8 (-2)]
CLIENT: <- Wait for packet of master server
- -> When id -2 (not found): Connect directly to the server
- -> When id -1 (found): Save the two strings, convert the port to a real and connect to server via UDP and the recieved ip&port (Knock-Knock) [packet: buffer_s8 (-3)]
SERVER: <- Wait for packet of master server
- -> When id -1 (found): Save the two strings, convert the port to a real and connect to client via UDP and the recieved ip&port (Knock-Knock) [packet: buffer_s8 (-3)]
<- Wait for incoming client Knock-Knock packet (-3):
- -> When not already connected with this client: add it to the connected clients and send a welcome packet via UDP back to the client that sent this packet [packet: buffer_s8 (-4)}
Without hole punching the client simply sends the Knock-Knock packets to the server and waits for the servers welcome message.
GMnet GATE PUNCH Manual¶
You can find the GMnet GATE PUNCH manual here. Please note, that the GMnet GATE PUNCH manual is outdated, as GATE PUNCH will soon be replaced with a new, rewritten, master server
Support¶
In case the manual couldn’t help you, feel free to go to our forums, we will help you :)
Contribute to this manual¶
This manual is open source. If you want to contribute to it, visit our Github repository.