About

This article is intended for intermediate Unity developers looking to integrate Unity Networking (UNET) with Steamworks peer-to-peer networking.

TL;DR: Show me the code!

Action shot of the example project

Action shot of the example project

UNET and Steam P2P

So you've already implemented your game's multiplayer features with UNET and, oh shit, it doesn't even support Steam P2P. WTF? 

Fret not! They're working on it. And for now, there are ways to trick UNET into working with Steam P2P. This is how I did it for our game Rival Megagun.

 

What's the point?

Lower cost.

This method is good for multiplayer games that use matchmaking and P2P networking but don't require dedicated servers. By using UNET and Steam's NAT-traversal and relay servers, you don't need to pay for Unity's multiplayer services or host your own facilitator/relay servers.

 

Is this the best method?

I don't know. 

This implementation makes some assumptions about the internals of UNET, so it may require some tweaking if UNET has any major updates. To fully understand it, you may need to dig into UNET's source code. I like this method because I didn't have to modify the UNET source code, or install any extra third party plugins, or pay for any extra services. 

 

How does it work?

See for yourself.

Essentially we override UNET's transport code to use the Steam P2P API. This allows UNET to continue to function as expected on the surface while using Steam to send and receive data. All of your UNET RPCs, NetworkBehaviours, NetworkMessages, etc. should continue to work as intended. 

This isn't a full tutorial. Just highlighting some examples. If you want to see the full working solution, check out my sample project. 

 

1. Connecting

1.a) Start UNET Server

Start the NetworkServer on the host machine and add a NetworkClient to represent the local client (just as you would with any UNET game). The NetworkServer should not actually listen on any port. This is because we don't transmit data through UNET, we do so through Steam P2P.

void StartUNETServer()
{
    // Start UNET server
    NetworkServer.Configure(SteamNetworkManager.hostTopology);
    NetworkServer.dontListen = true;
    NetworkServer.Listen(0);

    // Create a local client-to-server connection to the "server"
    // Connect to localhost to trick UNET's ConnectState state to "Connected", which allows data to pass through TransportSend
    myClient = ClientScene.ConnectLocalServer();
    myClient.Configure(SteamNetworkManager.hostTopology);
    myClient.Connect("localhost", 0);
    myClient.connection.ForceInitialize();

    // Add local client to our list of connections. Here we get the connection from the NetworkServer because it represents the server-to-client connection
    var serverToClientConn = NetworkServer.connections[0];
    connectedClients.Add(serverToClientConn);
}

 

1.b) Establish P2P Connection

Establish a P2P connection through Steam. This is done by first starting a Steam lobby on the host machine. Steam clients join the lobby and send a packet to the host to request a P2P connection. The host accepts this connection and sends a packet back as confirmation. 

A NetworkConnection is then instantiated on both machines to represent the P2P connection. You will need to create a class derived from NetworkConnection so that it can reference the peer's SteamID.  (e.g. SteamNetworkConnection)

Client:

// Called on Client by OnLobbyEntered
IEnumerator RequestP2PConnectionWithHost()
{
    var hostUserId = SteamMatchmaking.GetLobbyOwner (steamLobbyId);

    //send packet to request connection to host via Steam's NAT punch or relay servers
    SteamNetworking.SendP2PPacket (hostUserId, null, 0, EP2PSend.k_EP2PSendReliable);
  
   // wait for response from host
    uint packetSize;
    while (!SteamNetworking.IsP2PPacketAvailable (out packetSize)) {
        yield return null;
    }

    byte[] data = new byte[packetSize];
    CSteamID senderId;

    if (SteamNetworking.ReadP2PPacket (data, packetSize, out packetSize, out senderId)) 
    {
        if (senderId.m_SteamID == hostUserId.m_SteamID)
        {
            // packet was from host, assume it's notifying client that AcceptP2PSessionWithUser was called
            P2PSessionState_t sessionState;
            if (SteamNetworking.GetP2PSessionState (hostUserId, out sessionState)) 
            {
                // Connect to the unet server 
                // Create connection to host player's steam ID
                var conn = new SteamNetworkConnection(hostUserId);
                var mySteamClient = new SteamNetworkClient(conn);
                this.myClient = mySteamClient;

                // Setup and connect
                mySteamClient.SetNetworkConnectionClass<SteamNetworkConnection>();
                mySteamClient.Configure(SteamNetworkManager.hostTopology);
                mySteamClient.Connect();
            }

        }
    }

}

 

Host:

// Called on Host when a Client sends their first packet
void OnP2PSessionRequested(P2PSessionRequest_t pCallback)
{
    if (NetworkServer.active && SteamManager.Initialized) 
    {
        // Accept the connection if this user is in the lobby
        int numMembers = SteamMatchmaking.GetNumLobbyMembers(SteamLobbyID);

        for (int i = 0; i < numMembers; i++) 
        {
            var member = SteamMatchmaking.GetLobbyMemberByIndex (SteamLobbyID, i);

            if (member.m_SteamID == pCallback.m_steamIDRemote.m_SteamID)
            {
                // accept connection
                SteamNetworking.AcceptP2PSessionWithUser (pCallback.m_steamIDRemote);

                // send confirmation packet to peer
                SteamNetworking.SendP2PPacket (pCallback.m_steamIDRemote, null, 0, EP2PSend.k_EP2PSendReliable);

                // create new connnection for this client and connect them to server
                var newConn = new SteamNetworkConnection(member);
                newConn.ForceInitialize();

                NetworkServer.AddExternalConnection(newConn);
                connectedClients.Add(conn);

                return;
            }
        }
    }

}   

 

1.c) Initialization 

When starting the NetworkServer and instantiating NetworkClients and NetworkConnections, make sure you connect and initialize them properly. See my examples:

SteamNetworkClient.Connect()

UNETExtensions.ForceInitialize(NetworkConnection) 

UNETServerController.StartUNETServer ()

 

2. Sending data

Override NetworkConnection's TransportSend function to send data directly to the peer via the Steam P2P API. 

public class SteamNetworkConnection : NetworkConnection
{
    public CSteamID steamId;

    public SteamNetworkConnection() : base()
    {
    }

    public SteamNetworkConnection(CSteamID steamId)
    {
        this.steamId = steamId;
    }

    public override bool TransportSend(byte[] bytes, int numBytes, int channelId, out byte error)
    {
        if (steamId.m_SteamID == SteamUser.GetSteamID().m_SteamID)
        {
            // sending to self. short circuit
            TransportReceive(bytes, numBytes, channelId);
            error = 0;
            return true;
        }

        EP2PSend eP2PSendType = EP2PSend.k_EP2PSendReliable;

        QosType qos = SteamNetworkManager.hostTopology.DefaultConfig.Channels[channelId].QOS;
        if (qos == QosType.Unreliable || qos == QosType.UnreliableFragmented || qos == QosType.UnreliableSequenced)
        {
            eP2PSendType = EP2PSend.k_EP2PSendUnreliable;
        }

        // Send packet to peer through Steam
        if (SteamNetworking.SendP2PPacket(steamId, bytes, (uint)numBytes, eP2PSendType))
        {
            error = 0;
            return true;
        }
        else
        {
            error = 1;
            return false;
        }
    }

}


3. Receiving data

Poll for P2P packets and pass the data to the appropriate NetworkConnection's TransportReceieve function. UNET will handle the rest. 

void Update()
{
    if (!SteamManager.Initialized)
    {
        return;
    }

    if (!IsConnectedToUNETServer())
    {
        return;
    }

    uint packetSize;

    // Read Steam packets
    while (SteamNetworking.IsP2PPacketAvailable (out packetSize))
    {
        byte[] data = new byte[packetSize];

        CSteamID senderId;

        if (SteamNetworking.ReadP2PPacket (data, packetSize, out packetSize, out senderId)) 
        {
            NetworkConnection conn;

            if (UNETServerController.IsHostingServer())
            {
                // We are the server, one of our clients will handle this packet
                conn = UNETServerController.GetClient(senderId);
            }
            else
            {
                // We are a client, we only have one connection (the server).
                conn = myClient.connection;
            }

            if (conn != null)
            {
                // Handle Steam packet through UNET
                conn.TransportReceive(data, Convert.ToInt32(packetSize), 0);
            }

        }
    }

}

 

Where can I learn more?

If you're looking for some more information on this topic, I found these posts very helpful: here and here.

Learn more about UNET and Steamworks.NET.

Familiarize yourself with the UNET source code.

Check out my sample project.


About the author

Justin Rempel is an independent game developer currently working on Rival Megagun, the PVP versus shmup. Check it out at rivalmegagun.com and on Twitter

Comment