-->
Attention: This tutorial was written for Godot 3. Not only the information here contains several flaws, networking has changed significantly in Godot 4.
In this part we will create some rudimentary "main menu" screen. By rudimentary, I will not care about layout or visual style. Only the necessary widgets do deal with server creation or multiplayer joining. We then start coding the network.gd
and gamestate.gd
and then attach an script into the main_menu.tscn
root node in order to use the networking code.
The requirements here are not very big. We will "split the layout" in 3 sections.
That said, add 3 panel nodes just so we can better group those sections and, if needed, easily reposition each group within the screen. Name then as PanelPlayer
, PanelHost
and PanelJoin
.
Within the PanelPlayer
we provide a single line text input (LineEdit
node) named txtPlayerName
meant to get the player name. To make it clear in the UI what this input is about, above it we put a label saying "Player name:". Bellow the text input we add an sprite that will hold the default Godot icon. This could be any image really and even give room for avatar customization if desired. Nevertheless, we rename this sprite node to PlayerIcon
. Then we add a Color Picker Button
node, naming it btColor
, which allows the player to choose the "dominant" color of the icon. Select the btColor
and set the Color
property to be fully white, which means no color modulation will occur. Bellow it we add a common button named btDefaultColor
and Text = Default
, which will be used to reset the color to the default one. Above those buttons we add a label saying "Color". Note that I didn't rename every single widget node. Ideally we should but I only renamed those that we will have to reference from the GDScript. The node hierarchy looks like this:
Into the PanelHost
we need a single line text input to provide the server name. This node should be named txtServerName
. Another single line text for the listening port, named txtServerPort
. For its default value, enter 4546 or any other really. Just be careful to not use some common port in order to avoid conflict problems. An SpinBox
node named txtMaxPlayers
which will give us the maximum allowed connections. Set its min value to 2
and max value to 16
while setting the current value to 6
(which will be the default one - keep this number relatively low in order to help with testing at a later moment). A common button node is used to effectively create the server. It's then named btCreate
and its display text Create
. Several labels are placed in order to better tell what each control is meant to do. The hierarchy looks like this:
For the PanelJoin
we only require two single line text (LineEdit
node), one for the IP and the other for the joining port. We name then txtJoinIP
and txtJoinPort
, respectively. As default text we enter 127.0.0.1
in the txtJoinIP
, which is an special IP address value that points into the local machine. As for the default value of the txtJoinPort
, set it to the same value of txtServerPort
(4546). A button labeled Join
and named btJoin
will then be used to effectively try to join the server. The hierarchy looks like this:
And the actual menu looks like this (those colored lines are part of the top-left corner of the canvas):
There is one problem here, though. If you click the color picker button, the popup will appear bellow the "create server" and "join server" panels. To fix that we have to move the PanelPlayer
down bellow on the tree hierarchy:
Shortly we will attach an script into this menu and deal with the networking, which is the next thing we will work on.
There are several events that are sent by the Godot's networking system and we will want to listen to those in order to act accordingly. Those events are:
id
argument tells the unique ID of the newly connected machine. We will use this to perform some initial synchronization between the peers.There is one more event that I will not cover in this tutorial, which is used for custom packet data.
We will listen to all 5 listed events, so let's first create 5 "empty" functions in the network.gd
:
# Everyone gets notified whenever a new client joins the server
func _on_player_connected(id):
pass
# Everyone gets notified whenever someone disconnects from the server
func _on_player_disconnected(id):
pass
# Peer trying to connect to server is notified on success
func _on_connected_to_server():
pass
# Peer trying to connect to server is notified on failure
func _on_connection_failed():
pass
# Peer is notified when disconnected from server
func _on_disconnected_from_server():
pass
Now we can connect those functions to the event signals. In order to do that, we override the _ready()
function in the network class file:
func _ready():
get_tree().connect("network_peer_connected", self, "_on_player_connected")
get_tree().connect("network_peer_disconnected", self, "_on_player_disconnected")
get_tree().connect("connected_to_server", self, "_on_connected_to_server")
get_tree().connect("connection_failed", self, "_on_connection_failed")
get_tree().connect("server_disconnected", self, "_on_disconnected_from_server")
When we are in the main menu and click the Create
button within the create server panel, we will want to initialize the network, try to create the server and, if successful, assign that into the node tree. Shortly we will create a function that will make those tasks easier for us, and call that function from the "click event" of that button. In order to create the server itself we require the listening port as well as the maximum allowed connections. Those will be obtained from the widgets we have added previously, but rather than providing them to the function through arguments, we will setup some member variables. Or rather one member dictionary that will give us the necessary information. That said, let's declare it:
var server_info = {
name = "Server", # Holds the name of the server
max_players = 0, # Maximum allowed connections
used_port = 0 # Listening port
}
Within the function we will use the information stored in this dictionary. Ideally we should verify the validity of what is stored in there, but for simplicity sake, in this tutorial we wont. Prior to calling the function we will set those, but for now let's work on the new "Create Server Function". In it we first have to obtain a new network object. With it we then try to create the server, providing the port and max connections from the dictionary. If failed we just print the text, although in the end it should be replaced by a message box or something to tell the user the server creation has failed. Normally a failure here happens when the listening port is already in use in that machine. Anyway, the last task of the function is to assign the network object into the tree:
func create_server():
# Initialize the networking system
var net = NetworkedMultiplayerENet.new()
# Try to create the server
if (net.create_server(server_info.used_port, server_info.max_players) != OK):
print("Failed to create server")
return
# Assign it into the tree
get_tree().set_network_peer(net)
Now let's use this new function. We will call it once a button (btCreate
) in the main menu is clicked. This means we have to attach a new script into the main_menu.tscn
root node. Do that and name the new file main_menu.gd
. For the moment it should contain a single line, extends CanvasLayer
. Ok, now, select the btCreate
and within the Node
panel choose to create a new function connected to the Pressed
signal. Make sure it's connected to the CanvasLayer
node. The default function name given by the editor is fine.
In the created function we will set all 3 network.server_info
dictionary entries, gathering the values from the input widgets within the main menu. Although in the code I check if the server name is empty or not, ideally we should handle the text_changed(new_text)
signal and in there enable or disable the btCreate
button based on the conditions that all fields must be filled. For simplicity sake we just just take the values and that's it. Since port and the maximum amount of connections must be integers, we convert them from text. The resulting script should be pretty straightforward:
func _on_btCreate_pressed():
# Gather values from the GUI and fill the network.server_info dictionary
if (!$PanelHost/txtServerName.text.empty()):
network.server_info.name = $PanelHost/txtServerName.text
network.server_info.max_players = int($PanelHost/txtMaxPlayers.value)
network.server_info.used_port = int($PanelHost/txtServerPort.text)
# And create the server, using the function previously added into the code
network.create_server()
If you decide to test the game now, clicking the create button will apparently do nothing. Clicking it again will display (in the output panel) the "Failed to create server" message. This happens because the server is indeed created but we haven't done anything to make it clear about the fact. When pressed again it will try to use a port that is already in use. The thing we can do that will be the most obvious the server has successfully created is to transition into the game world scene. We will do so, but we need to know the server has been created. We can either return a "true" from the network.create_server()
function or emit a signal. I have chosen the second option because it will give a more consistent "success vs failure" handling throughout the project. The thing is, there will be cases that we wont be able to use return values (I will mention them at the relevant time).
That said, we have to move back to the network.gd
file and declare that this class will emit a signal. In this case we can name it server_created
:
signal server_created # when server is successfully created
And then, we update the create_server()
function to emit this signal:
func create_server():
# Initialize the networking system
var net = NetworkedMultiplayerENet.new()
# Try to create the server
if (net.create_server(server_info.used_port, server_info.max_players) != OK):
print("Failed to create server")
return
# Assign it into the tree
get_tree().set_network_peer(net)
# Tell the server has been created successfully
emit_signal("server_created")
Now that we have an event telling the server has been created, we have to handle it. First, we connect a function to it, from the _ready()
function in the main_menu.gd
class (don't worry, we will create the function shortly):
func _ready():
network.connect("server_created", self, "_on_ready_to_play")
Based on this code, we will use a function named _on_ready_to_play()
in order to handle the event. All we do in this function is change into the game world scene:
func _on_ready_to_play():
get_tree().change_scene("res://game_world.tscn")
And testing now, clicking the create button should move into the game world scene, which is completely empty for the moment. We will work on that in the next part. Nevertheless, we now know the server creation has worked, or at least it seems so!
Ok, we now have means to create a server, but how about joining one? We will follow the same strategy of creating a function in the network.gd
file meant to hold the joining code. In this case we will request the necessary data through function arguments, which are the IP and port. We won't use the server_info dictionary in here because those are meant to hold server information. Another thing to mention here is that joining server can't use return values to "tell" when we successfully got into a server or not. For that we have the connected_to_server
signal that is emitted by the network API provided by Godot. We are already handling this event with the _on_connected_to_server()
function. Since we want outside code to also react to the success (or failure) of connecting to a server, we will create another two signals, one named join_success
and another called join_fail
:
signal server_created # when server is successfully created
signal join_success # When the peer successfully joins a server
signal join_fail # Failed to join a server
Let's then work on the function used to try to join a server. In there we first initialize the network object, try to join a server using the given arguments (IP and port) and then we assign the network object into the node tree:
func join_server(ip, port):
var net = NetworkedMultiplayerENet.new()
if (net.create_client(ip, port) != OK):
print("Failed to create client")
emit_signal("join_fail")
return
get_tree().set_network_peer(net)
Notice the fact that we still don't know if the joining was successful or not. We are handling two events emitted by the network API that will tell us about the fact. In the case of the success, _on_connected_to_server()
, for the moment we will just emit our own join_success
signal. As for the failure, we emit our own join_fail
signal as well as clear the network object from the node tree:
# Peer trying to connect to server is notified on success
func _on_connected_to_server():
emit_signal("join_success")
# Peer trying to connect to server is notified on failure
func _on_connection_failed():
emit_signal("join_fail")
get_tree().set_network_peer(null)
We now can join a server, we just need to call this function from the main menu! Ok, we also need to listen to the signals telling about success or failure. So, let's first connect them. For the success we will reuse the _on_ready_to_play()
function, although it would be a good idea to separate that into a different function if there is any need to perform different tasks based on wether we are creating or joining a server. As for the failure we will create a new function shortly. The connections:
func _ready():
network.connect("server_created", self, "_on_ready_to_play")
network.connect("join_success", self, "_on_ready_to_play")
network.connect("join_fail", self, "_on_join_fail")
As for the _on_join_fail()
function, all we will do is print a message, which will appear in the output panel of the Godot editor. Of course, in a final product we would display an error message dialog to the player. Nevertheless, here is the code:
func _on_join_fail():
print("Failed to join server")
Lastly, we have to connect the btJoin
pressed signal and call the network.join_server()
function. As with the create server, we will gather the necessary information from the input widgets. That said, select the btJoin
button and, from the node panel, create a new connection to the pressed()
signal. The default function name is fine. The resulting code:
func _on_btJoin_pressed():
var port = int($PanelJoin/txtJoinPort.text)
var ip = $PanelJoin/txtJoinIP.text
network.join_server(ip, port)
To test the joining code we get exposed to one the limitations of the Godot at the moment. We can't ask it to open multiple instances of the game. What some people do is open the editor multiple times and then hit the play button on each editor window. This does have a problem that output and debugging messages become completely mixed between the various editor instances.
The way I have found to test is to first export the game into binary (luckily it's relatively fast) and then open the game from it at least once, while also having one of the testing instances opened from the editor. Since we are testing, I do recommend exporting with debug so a console will be open for each game instance, outputting the various print()
commands and some other engine information.
Since we didn't add anything to the game world, both the server and client will get an empty scene, which by default is gray. Nevertheless, we have fully functioning "create server" and "join server" capabilities!
In the next part we display something in the game world in order to properly test our code. We will also incorporate some internal peer management, which will make things a bit easier to deal with.