Skip to content

Tutorials - P2P Networking

One of the more requested tutorials is multiplayer lobbies and P2P networking through Steam; this tutorial specifically covers the P2P Networking portion and our lobbies tutorial covers the other half.

Please note this tutorial uses the older Steamworks Networking class and this is for a basic, turn-based lobby / P2P set-up. Only use this as a starting point.

I'd also like to suggest you check out the Additional Resources section of this tutorial before continuing on.

Relevant GodotSteam classes and functions

The _ready() Function

Next we'll want to set up the signal connections for Steamworks and a command line checker like so:

func _ready() -> void:
    Steam.connect("p2p_session_request", self, "_on_p2p_session_request")
    Steam.connect("p2p_session_connect_fail", self, "_on_p2p_session_connect_fail")

    # Check for command line arguments
    check_command_line()
func _ready() -> void:
    Steam.p2p_session_request.connect(_on_p2p_session_request)
    Steam.p2p_session_connect_fail.connect(_on_p2p_session_connect_fail)

    # Check for command line arguments
    check_command_line()

We will get into each of these below.

The _process() Function

We'll also need to set read_p2p_packet() in our _process() function so it is always looking for new packets:

func _process(_delta) -> void:
    Steam.run_callbacks()

    # If the player is connected, read packets
    if lobby_id > 0:
        read_p2p_packet()

If you are using the global.gd autoload singleton then you can omit the run_callbacks() command as they'll be running already.

Here is a nice bit of code from tehsquidge for handling packet reading:

func _process(delta):
    Steam.run_callbacks()

    if lobby_id > 0:
        read_all_p2p_packets()


func read_all_p2p_packets(read_count: int = 0):
    if read_count >= PACKET_READ_LIMIT:
        return

    if Steam.getAvailableP2PPacketSize(0) > 0:
        read_p2p_packet()
        read_all_p2p_packets(read_count + 1)

P2P Networking - Session Request

Next we'll check out the P2P networking functionality. Over in the lobby tutorial, we did a P2P handshake when someone joins the lobby, it would trigger a p2p_session_request callback which would in turn trigger this function:

func _on_p2p_session_request(remote_id: int) -> void:
    # Get the requester's name
    var this_requester: String = Steam.getFriendPersonaName(remote_id)
    print("%s is requesting a P2P session" % this_requester)

    # Accept the P2P session; can apply logic to deny this request if needed
    Steam.acceptP2PSessionWithUser(remote_id)

    # Make the initial handshake
    make_p2p_handshake()

It pretty simply acknowledges the session request, accepts it, then sends a handshake back.

Reading P2P Packets

Inside that handshake there was a call to the read_p2p_packet() function which does this:

func read_p2p_packet() -> void:
    var packet_size: int = Steam.getAvailableP2PPacketSize(0)

    # There is a packet
    if packet_size > 0:
        var this_packet: Dictionary = Steam.readP2PPacket(packet_size, 0)

        if this_packet.empty() or this_packet == null:
            print("WARNING: read an empty packet with non-zero size!")

        # Get the remote user's ID
        var packet_sender: int = this_packet['remote_steam_id']

        # Make the packet data readable
        var packet_code: PoolByteArray = this_packet['data']
        var readable_data: Dictionary = bytes2var(packet_code)

        # Print the packet to output
        print("Packet: %s" % readable_data)

        # Append logic here to deal with packet data
func read_p2p_packet() -> void:
    var packet_size: int = Steam.getAvailableP2PPacketSize(0)

    # There is a packet
    if packet_size > 0:
        var this_packet: Dictionary = Steam.readP2PPacket(packet_size, 0)

        if this_packet.is_empty() or this_packet == null:
            print("WARNING: read an empty packet with non-zero size!")

        # Get the remote user's ID
        var packet_sender: int = this_packet['remote_steam_id']

        # Make the packet data readable
        var packet_code: PackedByteArray = this_packet['data']
        var readable_data: Dictionary = bytes_to_var(packet_code)

        # Print the packet to output
        print("Packet: %s" % readable_data)

        # Append logic here to deal with packet data

If the packet size is greater than zero then it will get the sender's Steam ID and the data they sent. The line bytes2var (Godot 2x., 3.x) or bytes_to_var (Godot 4.x) is very important as it decodes the data back into something you can read and use. After it is decoded you can pass that data to any number of functions for your game.

Sending P2P Packets

Beyond the handshake, you will probably want to pass a lot of different pieces of data back and forth between players.

I have mine set up with two arguments: the first is the recipient as a string and the second is a dictionary. I think the dictionary is best for sending data so you can have a key / value pair to reference and make things less confusing on the receiving end. Each packet will go through the following function:

func send_p2p_packet(this_target: int, packet_data: Dictionary) -> void:
    # Set the send_type and channel
    var send_type: int = Steam.P2P_SEND_RELIABLE
    var channel: int = 0

    # Create a data array to send the data through
    var this_data: PoolByteArray
    this_data.append_array(var2bytes(packet_data))

    # If sending a packet to everyone
    if this_target == 0:
        # If there is more than one user, send packets
        if lobby_members.size() > 1:
            # Loop through all members that aren't you
            for this_member in lobby_members:
                if this_member['steam_id'] != steam_id:
                    Steam.sendP2PPacket(this_member['steam_id'], this_data, send_type, channel)

    # Else send it to someone specific
    else:
        Steam.sendP2PPacket(this_target, this_data, send_type, channel)
func send_p2p_packet(this_target: int, packet_data: Dictionary) -> void:
            # Set the send_type and channel
    var send_type: int = Steam.P2P_SEND_RELIABLE
    var channel: int = 0

    # Create a data array to send the data through
    var this_data: PackedByteArray
    this_data.append_array(var_to_bytes(packet_data))

    # If sending a packet to everyone
    if this_target == 0:
        # If there is more than one user, send packets
        if lobby_members.size() > 1:
            # Loop through all members that aren't you
            for this_member in lobby_members:
                if this_member['steam_id'] != steam_id:
                    Steam.sendP2PPacket(this_member['steam_id'], this_data, send_type, channel)

    # Else send it to someone specific
    else:
        Steam.sendP2PPacket(this_target, this_data, send_type, channel)

The send_type variable will corresponed to these enums and integers:

Send Type Enums Values Descriptions
P2P_SEND_UNRELIABLE 0 Send unreliable
P2P_SEND_UNRELIABLE_NO_DELAY 1 Send unreliable with no delay
P2P_SEND_RELIABLE 2 Send reliable
P2P_SEND_RELIABLE_WITH_BUFFERING 3 Send reliable with buffering

The channel used should match for both read and send functions. You may want to use multiple channels so this should obviously be adjusted.

As your game increases in complexity, you may find the amount of data you're sending increases significantly. One of the core tenets of responsive, effective networking is reducing the amount of data you're sending, to reduce the chance of some part becoming corrupted or requiring players of your game to have a very fast internet connection to even play your game.

Luckily, we can introduce compression to our send function to shrink the size of the data without needing to change our whole dictionary. The concept is straightforward enough; when we call the var2bytes (Godot 2.x, 3.x) or var_to_bytes (Godot 4.x) function, we turn our dictionary (or some other variable) into a PoolByteArray (Godot 2.x, 3.x) or PackedByteArray (Godot 4.x) and send it over the internet.

We can compress the PoolByteArray / PackedByteArray to be smaller with a single line of code:

func send_p2p_packet(target: int, packet_data: Dictionary) -> void:
    # Set the send_type and channel
    var send_type: int = Steam.P2P_SEND_RELIABLE
    var channel: int = 0

    # Create a data array to send the data through
    var this_data: PoolByteArray

    # Compress the PoolByteArray we create from our dictionary  using the GZIP compression method
    var compressed_data: PoolByteArray = var2bytes(packet_data).compress(File.COMPRESSION_GZIP)
    this_data.append_array(compressed_data)

    # If sending a packet to everyone
    if target == 0:
        # If there is more than one user, send packets
        if lobby_members.size() > 1:
            # Loop through all members that aren't you
            for this_member in lobby_members:
                if this_member['steam_id'] != steam_id:
                    Steam.sendP2PPacket(this_member['steam_id'], this_data, send_type, channel)

    # Else send it to someone specific
    else:
        Steam.sendP2PPacket(target, this_data, send_type, channel)
func send_p2p_packet(target: int, packet_data: Dictionary) -> void:
    # Set the send_type and channel
    var send_type: int = Steam.P2P_SEND_RELIABLE
    var channel: int = 0

    # Create a data array to send the data through
    var this_data: PackedByteArray

    # Compress the PackedByteArray we create from our dictionary  using the GZIP compression method
    var compressed_data: PackedByteArray = var_to_bytes(packet_data).compress(FileAccess.COMPRESSION_GZIP)
    this_data.append_array(compressed_data)

    # If sending a packet to everyone
    if target == 0:
        # If there is more than one user, send packets
        if lobby_members.size() > 1:
            # Loop through all members that aren't you
            for this_member in lobby_members:
                if this_member['steam_id'] != steam_id:
                    Steam.sendP2PPacket(this_member['steam_id'], this_data, send_type, channel)

    # Else send it to someone specific
    else:
        Steam.sendP2PPacket(target, this_data, send_type, channel)

Of course, we've now sent a compressed PoolByteArray / PackedByteArray to someone else over the internet, so when they receive the packet, they will need to first decompress the PoolByteArray / PackedByteArray before they can decode it. To accomplish this, we add a single line of code to our read_p2p_packet function like so:

func read_p2p_packet() -> void:
    var packet_size: int = Steam.getAvailableP2PPacketSize(0)

    # There is a packet
    if packet_size > 0:
        var this_packet: Dictionary = Steam.readP2PPacket(packet_size, 0)

        if this_packet.empty() or this_packet == null:
            print("WARNING: read an empty packet with non-zero size!")

        # Get the remote user's ID
        var packet_sender: int = this_packet['remote_steam_id']

        # Make the packet data readable
        var packet_code: PoolByteArray = this_packet['data']

        # Decompress the array before turning it into a useable dictionary
        var readable_data: Dictionary = bytes2var(packet_code.decompress_dynamic(-1, File.COMPRESSION_GZIP))

        # Print the packet to output
        print("Packet: %s" % readable_data)

        # Append logic here to deal with packet data
func read_p2p_packet() -> void:
    var packet_size: int = Steam.getAvailableP2PPacketSize(0)

    # There is a packet
    if packet_size > 0:
        var this_packet: Dictionary = Steam.readP2PPacket(packet_size, 0)

        if this_packet.is_empty() or this_packet == null:
            print("WARNING: read an empty packet with non-zero size!")

        # Get the remote user's ID
        var packet_sender: int = this_packet['remote_steam_id']

        # Make the packet data readable
        var packet_code: PackedByteArray = this_packet['data']

        # Decompress the array before turning it into a useable dictionary
        var readable_data: Dictionary = bytes_to_var(packet_code.decompress_dynamic(-1, FileAccess.COMPRESSION_GZIP))

        # Print the packet to output
        print("Packet: %s" % readable_data)

        # Append logic here to deal with packet data

The key point to note here is the format must be the same for sending and receiving. There's a whole lot to read about compression in Godot, far beyond this tutorial; to learn more, read all about it here.

P2P Session Failures

For the last part of this tutorial we'll handle P2P failures with the following function which is triggered by the p2p_session_connect_fail callback:

func _on_p2p_session_connect_fail(steam_id: int, session_error: int) -> void:
    # If no error was given
    if session_error == 0:
        print("WARNING: Session failure with %s: no error given" % steam_id)

    # Else if target user was not running the same game
    elif session_error == 1:
        print("WARNING: Session failure with %s: target user not running the same game" % steam_id)

    # Else if local user doesn't own app / game
    elif session_error == 2:
        print("WARNING: Session failure with %s: local user doesn't own app / game" % steam_id)

    # Else if target user isn't connected to Steam
    elif session_error == 3:
        print("WARNING: Session failure with %s: target user isn't connected to Steam" % steam_id)

    # Else if connection timed out
    elif session_error == 4:
        print("WARNING: Session failure with %s: connection timed out" % steam_id)

    # Else if unused
    elif session_error == 5:
        print("WARNING: Session failure with %s: unused" % steam_id)

    # Else no known error
    else:
        print("WARNING: Session failure with %s: unknown error %s" % [steam_id, session_error])

This will print a warning message so you know why the session connection failed. From here you can add any additional functionality you want like retrying the connection or something else.

Next Up

That concludes the P2P tutorial. At this point you may want to check out the lobbies tutorial (if you haven't yet) which compliments this one. Obviously this code should not be used for production and more for a very, very, very, simple guide on where to start.

Additional Resources

Suggested Reading Material

I highly suggest reading some or all of this to better understand networking.

Valve's networking documentation

'Game Networking Resources' by ThusSpokeNomad

'How to write a network game?' on StackOverflow

Networking by Gaffer On Games

'Client/Server Game Architecture' by Gabriel Gambetta

Video Tutorials

Godot 4 Steam Multiplayer' by Gwizz

'GodotSteamHL' by JDare

Example Project

To see this tutorial in action, check out our GodotSteam Example Project on GitHub. There you can get a full view of the code used which can serve as a starting point for you to branch out from.