Making peer-to-peer multiplayer seamless with Godot
When we started developing our recently-released mini-strategy game 2 Planets, we wanted to add networked multiplayer support. Since the game is a side project and would be released for free, we didn’t want to spend much time and money on maintaining dedicated servers. Instead, we chose a simple peer-to-peer networking setup where both players would connect to the other player’s machine directly.
Our initial implementation was very simple - players entered the IP of their match partner, we passed it to Godot and let the engine handle the rest. This had some problems:
- Entering IP addresses by hand is cumbersome and error-prone
- No matchmaking support
- Machines behind a NAT-enabled router are not accessible directly
To solve these problems, we set up a small “rendezvous” server capable of NAT hole punching. When starting a game, players connect to the publicly available server, which sends them the IP address of their match partner and punches holes into router’s NATs if needed. Both game clients then connect directly to each other, and the server’s job is done. This process allowed us to connect players using small “Match codes” which were shorter and easier to type than IP addresses. We haven’t implemented matchmaking yet, but adding it to the current implementation would be a simple task.
Two players starting a game over the network in 2 PlanetsI consider this approach ideal for small indie games. Dedicated servers allow better cheat protection, and NAT hole punching doesn’t work all the time, but with this approach, the maintenance burden and complexity is considerably lower, both for the game’s code and the server’s operation.
If you’re looking for a way to have peer-to-peer multiplayer without setting up your own server, have a look at Steam’s Networking API which also has a Godot integration via GodotSteam, or have a look at the Epic’s NAT P2P API. Both provide NAT hole punching support with relay services as a fallback.
If, on the other hand, you want to stay independent of Steam and Epic or just like to do things yourself, continue reading! In the following you’ll find a short guide on setting up peer-to-peer multiplayer with NAT hole-punching in Godot for your own game, using your own server.
Setting up your own
You’ll need the following:
- The excellent HolePuncher Godot plugin and server by Cregg Hancock
- A server or service allowing you to host a small python application (e.g. Heroku or Vercel)
- Basic networking and server administration knowledge. Using a service like Heroku, you can get a python server up and running pretty easily, but you’ll still need some experience to configure everything correctly.
- An understanding of Godot’s multiplayer API for connecting clients using the IP obtained from the rendezvous server and synchronizing the game state.
First, deploy the HolePuncher python server. Make sure it is accessible from your development machine. You don’t need a domain: if your server has a static IP, you can use that to let your clients initiate a connection.
Next, download and include the HolePuncher Godot plugin in your game. Copy the addons
folder from the HolePuncher repository and place it at the root of your Godot project folder. Afterwards, go to Project > Project Settings > Plugins and enable the HolePuncher plugin.
You can now add and configure a HolePunch
node to your menu scene.
We’ll do that in the following script:
hole_puncher = preload('res://addons/Holepunch/holepunch_node.gd').new()
# your rendezvous server IP or domain
hole_puncher.rendevouz_address = "1.1.1.1"
# the port the HolePuncher python application is running on
hole_puncher.rendevouz_port = "3000"
add_child(hole_puncher)
Refer to the HolePuncher documentation to learn more about its configuration.
You can now start the NAT traversal by calling the hole puncher’s start_traversal
method:
# Generate a unique ID for this machine
var player_id = OS.get_unique_id()
hole_puncher.start_traversal(game_code, is_host, player_id)
# Yield an array of [own_port, host_port, host_ip]
var result = yield(hole_puncher, 'hole_punched')
Run this code on both machines, with is_host
set to true
on one machine, and to false
on the other.
The game_code
variable is the match identifier, telling the server which peers’ IP addresses belong together. You can use any format, as long as it is unique for each match and both peers in a match use the same value. For an example on how to generate random game codes, look at this function in the 2 Planets source code.
After generating, you can display the game code you generated in your menu for players to exchange it through voice or chat messaging.
The yield
call above turns your code into a coroutine which will continue running after all the network calls have succeeded, which can take a few seconds.
When the hole puncher is done, you can use the returned information to create a Godot network peer.
You might use it like this:
# Start a host
var result = yield(hole_puncher, 'hole_punched')
var my_port = result[0]
var peer = NetworkedMultiplayerENet.new()
peer.create_server(my_port, 1)
get_tree().set_network_peer(peer)
# Connect a client to a host
var result = yield(hole_puncher, 'hole_punched')
var host_ip = result[2]
var host_port = result[1]
var own_port = result[0]
var peer = NetworkedMultiplayerENet.new()
peer.create_client(host_ip, host_port, 0, 0, own_port)
get_tree().set_network_peer(peer)
After starting a host on one machine and connecting a client on another, you can start the game! Congratulations, you now have peer-to-peer multiplayer support from (almost) any network!
Of course, this example only works for two players and is heavily simplified, you’ll need to add at least a way for players to exchange game codes and handle errors that appear during connection attempts. If you’d like to see a more realistic example, have a look at the lobby code from 2 Planets. If you’re interested in adding matchmaking, have a look at the source code of the HolePuncher server. It’s pretty compact.
I hope this guide was a good starting point for adding NAT hole punching to your game. NAT hole punching is a complex topic and you will probably have to look into other resources for a robust solution that fits your use-case, but getting started is surprisingly easy! If you have any questions or comments, feel free to reach out on twitter or reddit.