Introduction
This article is intended for people who want to learn how to use Godot 3 to make a multiplayer game with no experience in the network field.The magic with Godot is that the majority of the difficulty with multiplayer is hidden by the engine itself.
I’ve attached a sample game with source code, you can download it and read it to better understand how it works. This program is pretty easy to use, you launch one instance as a server and many others as clients that connect to the server.
Even though I’ve been developing games for several decades and have used multiple engines in the past, my experience with Godot is quite recent (only a few months), so if you see anything horrible in the code, please let me know.
https://github.com/cobolfoo/godot3-spacegame/
Description of the game
It’s an arcade top-down space combat game. You use WASD to move and SPACEBAR to shoot. When you die, you reappear at a random location. A server is used to host the game space. Clients connect to the server to control a ship. There is a login form that asks for the player’s name and the color of the ship.
First, let’s start with the server:
To start a server, it’s pretty easy. You create a new scene containing any kind of node (I used Node). You create a script and attach to this node. You put this code in your script:
func _ready():
var peer = NetworkedMultiplayerENet.new()
var result = peer.create_server(5555, 32)
get_tree().set_network_peer(peer)
get_tree().connect("network_peer_connected", self, "player_connected")
get_tree().connect("network_peer_disconnected", self, "player_disconnected")
func player_connected(id):
print("Callback: player_connected:", id)
func player_disconnected(id):
print("Callback: player_disconnected:", id)
This code create a server listening on port 5555 and allow up to 32 connections. If someone connect, player_connected function will be called. The same for disconnection with the player_disconnect function.That’s it, you have a basic server running now.
On the client:
This is pretty much the same thing, you create a scene, add a node and a script, then you type:
func _ready():
var peer = NetworkedMultiplayerENet.new()
peer.create_client("127.0.0.1", 5555)
get_tree().set_network_peer(peer)
get_tree().connect("connected_to_server", self, "client_connected_ok")
get_tree().connect("connection_failed", self, "client_connected_fail")
get_tree().connect("server_disconnected", self, "server_disconnected")
func client_connected_ok():
print("Callback: client_connected_ok")
func server_disconnected():
print("Callback: server_disconnected")
func client_connected_fail():
print("Callback: client_connected_fail")
This code connect to a server at 127.0.0.1 (Local host) on port 5555. If you get connected to the server, the client_connected_ok function will be called, client_connected_fail function will be called otherwise. The server_disconnected function is called when you get disconnected from the server.
It’s all about communication:
This is pretty good to have a server and a client working but now what? We need to make them communicate.
In Godot, the primary mean of communication is via RPC (Remote Procedure Call) functions. In the client, you can change the content of the client_connected_ok function for this:
func client_connected_ok():
var my_info = { name = "Player", color = Red" }
rpc_id(1,"register_player", get_tree().get_network_unique_id(), my_info)
rpc_id is a function used to send reliable data, the first parameter is the target peer ID, 1 is always the server. The second parameter is the RPC function name to call on the server, everything else after are optional parameters.
In this case, the parameters are my own unique peer ID (each connection have a different one) and an array containing my player name and color. I could have put the peer ID in the my_info array, it’s up to you to define the way you want to send the information.
On the server side, you just need to add this function to handle the data:
remote func register_player(id, info):
print("Remote: register_player(" + str(id) +","+str(info)+")")
Did you notice the remote keyword? In our case, this keyword means that register_player is an RPC function. This is pretty simple to use. You can use this keyword on the client side too, to catch data from RPC functions sent from the server.
The game use this for all the reliable communications between the server and connected clients. If you check the source code, in server.gd file, you can see that the server use rpc_id() function in a loop to tell other connected players that your client just registered a new player.
Handling client input and synchronizing nodes
The approach used by the game is straight-forward: the client send RPC functions every time the player press one of the keys (WASD for movement, spacebar for firing projectiles) to the server. The server is authoritative. Upon receiving player input, the server choose what to do. The client receive information from the server and render the result. This approach is a secure one and additional input validation can be done on the server side.
The server also send spaceship information (such as position and heading) to every connected clients (20 times per second). The information is sent using a RPC function, but this time we use rpc_unreliable_id function, sending unreliable RPC functions is way faster than reliable ones! We don’t care if something is lost since we always deal with the latest information available. Please do check the broadcast_world_positions function in server.gd to see it in action.
You want it as smooth as possible
We update spaceship positions 20 times per second but we render at 60 frames (or more) per second. The server runs the simulation but not the client. Do you see the problem? Spaceships will appear like they are jumping from a position to another! It will be a pretty crappy experience from the player’s perspective.
The method used in this game to mitigate this issue is to interpolate between the last two prior states. Instead of updating nodes upon receiving updates from server, we store the data and the arrival time in an array. Then we use the last 2 received states and interpolate between the values. Please check the _process function in the client.gd file to learn more about it.
The main drawback of this approach is that you get a 100 ms delay on what is really happening on the server (plus the network delay between client and server). Depending on the game genre, it might not be acceptable. You can increase the number of updates per second or use another approach such as extrapolating values and smoothly correcting errors over time.
Conclusion:
I hope you liked this article and that you have a better understanding about how multiplayer is done in Godot 3. There are some topics I left out of this article such as master node ownership because I don’t use it in my code anyway, if you want to know more, go read the official Godot documentation (check Further Readings below). I also plan to update the game over time to include additional features such as lag compensation technics and instant input handling.
Have fun coding your multiplayer game!
Further readings:
I would recommend you read the official Godot documentation about their high-level multiplayer, it’s pretty well explained:
https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html
In case you want to know a bit more about the dirty part of multiplayer networking:
https://gafferongames.com/categories/game-networking/