To establish a UDP connection, we need two core things:
- The IP address of the peers, mainly the server
- The port over which they will exchange data
For test purposes, on all our projects we are going to use the localhost
IP address. This is a shortcut to your local IP address mask. An IP address is like a house or apartment address. It is the exact location to which a given packet should be delivered and represents the address of the computer in the network. A port is essentially a specific channel in which the host allows a given communication to be established; we’ll use the 9999
as our default port. There’s nothing special about this one; it’s just an arbitrary pick.
With this in mind, let’s see for the first time the ENetMultiplayerPeer
class in action. As you can imagine, this setup requires a two-sided approach. We need to set up a game architecture for our server and a different architecture for our client.
Let’s start with the server architecture.
Creating the server
The ENetMultiplayerPeer
class in the Godot Engine provides a convenient way to create and manage network connections for online multiplayer games. One of the most important methods of this class is the create_server()
method, which is used to create a new server that can accept connections from clients. This method is simple to use and, besides having five arguments, it only requires one to get started:
- The first argument of the
ENetMultiplayerPeer.create_server()
method is the port on which the server will listen for incoming connections. This is the port number that clients will use to connect to the server. For example, if you want the server to listen on port 9999
, you would call ENetMultiplayerPeer.create_server(9999)
. This is the only mandatory argument to call this method.
- The second argument is
max_clients
, which is the maximum number of clients that the server will allow to connect at the same time. This argument is optional, and if not specified, the server will allow up to 4,095 clients to connect.
- The third argument is
max_channels
, which is the maximum number of channels we allow the server to use per client. Channels are used to separate different types of data, such as voice and video, and are useful for creating multiple streams of data within a single connection. This argument is optional, and if not specified, the server will allow an unlimited number of channels.
- The fourth argument is
in_bandwidth
, which is the maximum incoming bandwidth that the server will allow per client. This argument is optional, and if not specified, the server will allow unlimited incoming bandwidth.
- The fifth argument is
out_bandwidth
, which is the maximum outgoing bandwidth that the server will allow per client. This argument is optional, and if not specified, the server will allow unlimited outgoing bandwidth.
Let’s create our server in Godot Engine. Open up the project provided in the GitHub link given previously. After opening the project, execute the following steps:
- Create a new scene and use a
Node
instance as the root.
- Attach a new GDScript to this node, and name it
Server.gd
.
- Save the scene and open the script.
- Define a constant called
PORT
and set it to our default port number so the server can listen to it:const PORT = 9999
- Create a new
ENetMultiplayerPeer
using the new()
constructor. Let’s store it in a variable called peer
:var peer = ENetMultiplayerPeer.new()
- In the
_ready()
function, call the create_server()
method on the peer
variable, passing in the PORT
constant as an argument:func _ready():
peer.create_server(PORT)
- Still in the
_ready()
callback, assign the peer
variable to the built-in multiplayer
member variable of this node: multiplayer.multiplayer_peer = peer
- Connect the
peer_connected
signal of the multiplayer
variable to a function called _on_peer_connected
. We’ll create this callback method next:multiplayer.peer_connected.connect(_on_peer_connected)
- Create a new method called
_on_peer_connected()
, which should receive peer_id
as the argument:func _on_peer_connected(peer_id):
- In the
_on_peer_connected()
function, use print()
to print the passed peer_id
argument on the console: print(peer_id)
The complete script should look like this:
extends Node
const PORT = 9999
var peer = ENetMultiplayerPeer.new()
func _ready():
var error = peer.create_server(PORT)
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect
(_on_peer_connected)
func _on_peer_connected(peer_id):
print(peer_id)
It’s important to note that this script uses the built-in multiplayer
member variable that every Node
instance has on Godot Engine 4.0 Network API, which is an instance of the MultiplayerAPI
class.
Done: we have our server ready. Told you it would be simple!
Creating the client
Next up, let’s create our client. The process is quite similar. The major difference is that the client needs the server IP address to find it on the network.
We use the ENetMultiplayerPeer.create_client()
method to connect a client to a server. This method is very simple to use and requires only two arguments to work:
- The first argument of the
create_client()
method is the address of the server. This can be either the server’s IP or hostname. For instance, if you want the client to connect to a server with the IP address 192.168.1.1
, you would call create_client("192.168.1.1")
. But to make things simpler, we’ll use "localhost"
, which is a shortcut to our own IP address mask.
- The second argument of the
create_client()
method is the port on which the server is listening for incoming connections. This is the port number that the client will use to connect to the server. For example, if the server is listening on port 9999
, you would call create_client("192.168.1.1", 9999)
.
- The third argument of the
create_client()
method is channel_count
, which is the number of channels that the client will use to communicate with the server. Channels are used to separate different types of data, such as voice and video, and are useful for creating multiple streams of data within a single connection. This argument is optional, and if not specified, the client will use a default value of 1
channel.
- The fourth argument of the
create_client()
method is in_bandwidth
, which is the maximum incoming bandwidth that the client will allow per connection. This argument is optional, and if not specified, the client will use a default value of 0
, allowing an unlimited incoming bandwidth.
- The fifth argument of the
create_client()
method is out_bandwidth
, which is the maximum outgoing bandwidth that the client will allow per connection. This argument is optional, and if not specified, the client will use a default value of 0
, allowing an unlimited outgoing bandwidth.
- The sixth argument of the
create_client()
method is local_port
, which is the local port that the client will bind to. This argument is optional, and if not specified, the client will use a default value of 0
.
Now, let’s see how we can create the client side of this connection so it can connect with our server and establish their handshake:
- Create a new scene and add a
Node
instance as the root.
- Attach a new script to it.
- Save the script as
Client.gd
.
- In the script, define a constant called
ADDRESS
and set it to the server’s IP. In this case, we are going to use "localhost"
:const ADDRESS = "localhost"
- Define a constant called
PORT
and set it to be our default port number. It’s very important that this matches the number we used in Server.gd
, otherwise these peers won’t be able to find each other:const PORT = 9999
- Create a new
ENetMultiplayerPeer
using the new()
constructor and store it in a variable called peer
:var peer = ENetMultiplayerPeer.new()
- In the
_ready()
callback, call the create_client()
method on the peer
variable, passing in the ADDRESS
and PORT
constants as arguments:func _ready():
peer.create_client(ADDRESS, PORT)
- Assign the
peer
variable to the built-in multiplayer
member variable of the node:multiplayer.multiplayer_peer = peer
The complete script should look like this:
extends Node
const ADDRESS = "localhost"
const PORT = 9999
var peer = ENetMultiplayerPeer.new()
func _ready():
peer.create_client(ADDRESS, PORT)
multiplayer.multiplayer_peer = peer
Alright, we have our server and our client ready. Now, how do we test them?
Testing our handshake
Godot Engine 4.0 has a useful feature for debugging: the ability to open multiple independent instances of the game. This feature allows us to test different scenes at the same time, making the debugging process much easier and faster.
To open multiple instances of the game, we need to select one option from up to four options in the Debug | Run Multiple Instances menu.
Figure 1.5 – The Run Multiple Instances menu
Then, as soon as we press the Run Project or Run Current Scene button, Godot will launch the instances we’ve set previously. Let’s stick with two instances for this project.
This feature is incredibly useful for testing online multiplayer games, as it allows us to open a server and a client in the same run. But, as you can see, it’s not very straightforward. When we run the project, it actually opens two instances of the same scene.
Let’s create a minimal menu where we can select whether we are a client or a server:
- Create a new scene and use
Control
as the root and name it MainMenu
.
- Add a
Label
node as a child of the root node.
- Add two
Button
nodes as children of the root node.
- Give the first
Button
the name ClientButton
and the second one ServerButton
:
Figure 1.6 – MainMenu’s Scene tree structure
- Set the
Button
nodes’ text
properties to I’m a client and I’m a server respectively and position them side by side in the middle of the screen.
- Set the
Label
node’s text
property to Are you a… and position it in the middle of the screen.
Figure 1.7 – MainMenu’s scene UI
- Attach a new GDScript instance to the
MainMenu
node and open it.
- Connect the ClientButton’s pressed signal to a function called
_on_client_button_pressed
.
Figure 1.8 – ClientButton’s pressed signal connection
- Connect the pressed signal of the ServerButton to a function called
_on_server_button_pressed
.
- In the
_on_client_button_pressed()
callback, let’s call the change_scene_to_file()
method on the get_tree()
instance, passing in "res://Client.tscn"
as the argument:extends Control
func _on_client_pressed():
get_tree().change_scene_to_file
("res://Client.tscn")
- In the
_on_server_button_pressed()
callback, do the same as before, passing "
res://Server.tscn"
instead.The complete script should look like this:
extends Control
func _on_client_pressed():
get_tree().change_scene_to_file
("res://Client.tscn")
func _on_server_pressed():
get_tree().change_scene_to_file("res://Server.tscn")
Now, let’s make sure we save the scene before we test it. After that, all we need to do is hit the Run Current Scene button and watch the scene come to life. All the hard work has been done, and now all that’s left is to appreciate the results.
Once we have the two debug instances running, we need to pick one to be the server first. For that, we can press ServerButton. This will launch our Server.tscn
scene and start listening for incoming connections.
Then, in the other instance, we need to press ClientButton. This will launch the Client.tscn
scene and try to connect to the server. If everything goes as expected, we should get peer_id
printed in the console of the server instance.
This means that the client and the server have successfully established a connection and are now ready to start exchanging messages. Congratulations, you’ve just created your first handshake!