Broadcast a LAN Multiplayer Game

Code and workflows to send and receive broadcast packets to join multiplayer sessions

What you'll develop on this page

Our server will broadcast its IP address over LAN and clients will be able to see all LAN servers broadcasting so they can select and join.

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Broadcasting-and-Joining-on-LAN

Creating UdpConnection

UdpClient

We are going to create a new class called UdpConnection which will have methods to both send and receive UDP broadcast packets. Our implementation works off of MattijsKneppers' great start in the Unity forums.

The core component of broadcasting will be UdpClient from .NET. It is important to note that this broadcasting method is not "Unity-approved"; we are using a Microsoft technology to get this done.

On the server side we will create a new UdpClient. We will create a "sendToEndpoint," which will be our broadcasting IP address and broadcasting port. We will then use UdpClient's "send" method to send data over to that endpoint.

The broadcasting IP address we recommend is "255.255.255.255"

There is material online that says this is a bad address to use because some routers ignore broadcasts to this address. This has not been the case in our testing.

If you find that IP address to be wonky in your development, or know of a better approach, please let us know on our Discord.

On the client side we will also create a UdpClient. We will bind on the broadcasting port to receive the messages sent in a broadcast at that port. We will then listen for messages.

Doesn't this mean that we'll just be broadcasting to ourselves and as a result we'll just be constantly receiving the messages we just sent, because we are binding and sending on the same port?!

Great point! And that's why we will only create a UdpConnection to broadcast in MainScene (the game) when we are the server. There'll be no clients that will also be broadcasting when joining the server. We will only be "listening" for broadcasts in NavigationScene (title menu).

For testing on the same machine we will make a build, then hardwire a port change for our editor player just to check if it works.

Thread

Listening for broadcast packets is a "blocking" activity, which means that this activity blocks a thread while it awaits a message.

We do not want to block all of Unity while we listen for broadcast messages. So, we're going to create a separate thread to handle listening for messages. Enter: Microsoft's Thread class.

Although this approach is also not "sanctioned" by Unity, it has never caused an issue throughout all of our testing.

Serialized JSON

Finally, what kind of message are we sending?

We will be serializing our data as JSON objects which contain - game name - server IP address - timestamp of when message was sent (server tick)

We will be serializing and deserializing our data using JsonUtility. This approach is a common pattern shown in the Unity docs.

NETWORKING IS HARD

We have tested this broadcasting on Windows/Linux/Mac desktops and iOS devices.

The broadcasting/pick up works great in "normal" cases (super majority of cases). "Normal" cases mean when devices are using "built in" wifi or ethernet.

We have gotten mixed results with "wacky" cases. "Wacky" cases are like a desktop computer using a usb dongle for wifi, or like if you are connecting through a VM so everything is "virtualized." In these types of cases, you may experience trouble with broadcasting or picking up broadcasts when using our methods in this gitbook.

Why? Well when machines have a Network Interface Controller (NIC) and additional methods to connect to the internet, it is hard to "automatically" know which IP endpoint to bind to. (Don't worry you don't need to know this stuff, just know "wacky" situations mean it's hard to know the right answer without asking the user).

To handle these "wacky" situations we can: 1) build a UI component into the app that asks the user and potentially add confusion, 2) program a more robust sending mechanism that sends across all interfaces or, 3) choose the most likely one and hope for the best.

In this gitbook,we went with approach #3. (If it turns out that we chose the wrong option for you all, please let us know in our Discord and we will update).

Additionally, you may find (like we did) a lot of answers on Stack Overflow telling you not to do broadcasting, and instead to do multicasting! It is "way better and recommended!"

Do not listen to those charlatans! You will spend countless hours testing, and retesting, and then after days of using Wireshark, and tracing packets, and learning more than you would ever like about networking data layers and IGMP packets, and learn that it is completely unknown how many routers support this functionality and that there is no way to really configure a router to see if it can support it.

Oh us? No, no we're not bitter....

UdpConnection

Without further ado...

  • Create a new script in the Multiplayer Setup folder called UdpConnection

  • Paste this code snippet into UdpConnection.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Text;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Threading;


// 
// "silent hero award" for this implementation
// https://forum.unity.com/threads/simple-udp-implementation-send-read-via-mono-c.15900/#post-3645256
// 
public class UdpConnection
{
    //In this variable we will store our actual udpClient object (from Microsoft)
    private UdpClient udpClient;

    //This is the broadcast address that we will be passed from the server for where to send our messages
    private string sendToIp;
    //This is the broadcast port, we either bind to it and listen on it as a client
    //or we bind to it and SEND to it as the server
    //It actually doesn't matter whhat port we bind on as the server, as long as we send to this port
    //but we decided to just bind to it as well to keep track of less numbers (it does mean we need to do)
    private int sendOrReceivePort;
 
    //This is a Queue of our messages (where we store received broadcast messages)
    private readonly Queue<string> incomingQueue = new Queue<string>();
    //This is the thread we will start to "listen" on
    Thread receiveThread;
    //We need to know if we were listening on a thread so we know whether to turn it off when we don't need it
    //If we are the server this will stay false
    private bool threadRunning = false;
    //The server will need to find its IP address so it can send it out to clients
    private IPAddress m_MyIp;

    //We call this method as a way to initialize our UdpConnection
    //We pass through the broadcast address (sendToIp) and the broadcast port (sendOrReceivePort)
    public void StartConnection(string sendToIp, int sendOrReceivePort)
    {
        //We create our udpClient by binding it to the sendOrReceivePort
        //Binding to the broadcast port really only matters if you are a client listening for our broadcast messages
        //The server could actually bind to any port
        try { udpClient = new UdpClient(sendOrReceivePort); }
        catch (Exception e)
        {
            Debug.Log("Failed to listen for UDP at port " + sendOrReceivePort + ": " + e.Message);
            return;
        }
        // "best tip of all time award" to MichaelBluestein
        // https://forums.xamarin.com/discussion/comment/1206/#Comment_1206
        // somehow you get IP address
        foreach (var netInterface in NetworkInterface.GetAllNetworkInterfaces()) {
            if (netInterface.OperationalStatus == OperationalStatus.Up &&  
                netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
                netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet) {
                foreach (var addrInfo in netInterface.GetIPProperties().UnicastAddresses) {
                    if (addrInfo.Address.AddressFamily == AddressFamily.InterNetwork) {

                        //We will use this address to broadcast out to clients as the server
                        m_MyIp = addrInfo.Address;
                    }
                }
            }  
        }
        //Now we configure our udpClient to be able to broadcast
        udpClient.EnableBroadcast = true;

        //We set our broadcast IP and broadcast port
        this.sendToIp = sendToIp;
        this.sendOrReceivePort = sendOrReceivePort;
    }
 
    //This will only be called by the client in order to start "listening"
    public void StartReceiveThread()
    {
        //We create our new thread that be running the method "ListenForMessages"
        receiveThread = new Thread(() => ListenForMessages(udpClient));
        //We configure the thread we just created
        receiveThread.IsBackground = true;
        //We note that it is running so we don't forget to turn it off
        threadRunning = true;
        //Now we start the thread
        receiveThread.Start();
    }
 
    //This method is called by StartReceiveThread()
    private void ListenForMessages(UdpClient client)
    {
        //We create our listening endpoint
        IPEndPoint remoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0);
 
        //We will continue running this until we turn "threadRunning" to false (which is when we don't need to listen anymore)
        while (threadRunning)
        {
            try
            {
                //A little console log to know we have started listening
                Debug.Log("starting receive on " + m_MyIp.ToString() +" and port " +sendOrReceivePort.ToString());
                
                // Blocks until a message returns on this socket from a remote host.
                Byte[] receiveBytes = client.Receive(ref remoteIpEndPoint);
                //We grab our byte stream as UTF8 encoding
                string returnData = Encoding.UTF8.GetString(receiveBytes);
                
                //We enqueue our received byte stream 
                lock (incomingQueue)
                {
                    incomingQueue.Enqueue(returnData);
                }
            }
            //Error handling
            catch (SocketException e)
            {
                // 10004 thrown when socket is closed
                if (e.ErrorCode != 10004) Debug.Log("Socket exception while receiving data from udp client: " + e.Message);
            }
            catch (Exception e)
            {
                Debug.Log("Error receiving data from udp client: " + e.Message);
            }

            //We take a pause after receiving a message and run it again
            Thread.Sleep(1);
        }
    }
 
    //This is another method the client will call to grab all the messages that have been received by listening
    public ServerInfoObject[] getMessages()
    {
        //We created an array of pending messages
        string[] pendingMessages = new string[0];
        //We create an array where we will store our ServerInfoObjects (how we store the JSON)
        ServerInfoObject[] pendingServerInfos = new ServerInfoObject[0];
        //While we get this done we need to lock the Queue
        lock (incomingQueue)
        {
            //We set our pending messages the length of our queue of byte stream
            pendingMessages = new string[incomingQueue.Count];
            //We set our pending server infos the length of our queue of byte stream
            pendingServerInfos = new ServerInfoObject[incomingQueue.Count];
            
            //We will go through all our messages and update to get an array of ServerInfoObjects
            int i = 0;
            while (incomingQueue.Count != 0)
            {
                //We start moving data from the queue to our pending messages
                pendingMessages[i] = incomingQueue.Dequeue();
                //We then take our pending message
                string jsonObject = pendingMessages[i];
                //And use FromJson to create our array of ServerInfoObjects
                pendingServerInfos[i] = JsonUtility.FromJson<ServerInfoObject>(jsonObject);
                i++;
            }
        }
        //We return an array of ServerInfoObjects to the client that called this method
        return pendingServerInfos;
    }

    //We will only call this method on the server and provide it the game name and time
    //We don't provide the game name in StartConnection because the client won't have it and 
    //both the client and server use StartConnection
    public void Send(float floatTime, string gameName)
    {
        //All our values need to be string to be able to be serialized
        string stringTime = floatTime.ToString();

        //We need to create our destination endpoint
        //It will be at the provided broadcast IP address and port
        IPEndPoint sendToEndpoint = new IPEndPoint(IPAddress.Parse(sendToIp), sendOrReceivePort);

        //We create a new ServerInfoObject which we will use to store our data
        ServerInfoObject thisServerInfoObject = new ServerInfoObject();
        //We populate our ServerInfoObject with data
        thisServerInfoObject.gameName = gameName;
        thisServerInfoObject.ipAddress = m_MyIp.ToString();
        thisServerInfoObject.timeStamp = stringTime;

        //Then we turn it into JSON
        string json = JsonUtility.ToJson(thisServerInfoObject);
        //Then we create a sendBytes array the size of the bytes of the JSON
        Byte[] sendBytes = Encoding.UTF8.GetBytes(json);
        //We then call send method on udpClient to send our byte array
        udpClient.Send(sendBytes, sendBytes.Length, sendToEndpoint);
    }
 
    public void Stop()
    {
        // Not always UdpClients are used for listening
        // Which is what requires the running thread to listen
        if (threadRunning == true)
        {
            threadRunning = false;
            receiveThread.Abort();
        }
        udpClient.Close();
        udpClient.Dispose();
    }
}

//This is our ServerInfoObject that we will be using to help us send data as JSON
[Serializable]
public class ServerInfoObject
{
    public string gameName = "";
    public string ipAddress = "";
    public string timeStamp = "";

}

We have a class that allows either the server to send messages or a client to receive messages

  • We created UdpConnection

Broadcasting from the server

Now that we have UdpConnection we need to create a script to pull the broadcast IP address, port, and game name from ClientServerInfo and send a broadcast message

  • Navigate to MainScene, right-click on the Hierarchy, and create an empty GameObject called GameBroadcasting

  • With GameBroadcasting selected in Hierarchy, click "Add Component" in the Inspector and make a new script called "GameServerBroadcasting"

    • Move GameServerBroadcasting into the Multiplayer Setup folder

  • Paste the code snippet below into GameServerBroadcasting.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Threading;

public class GameServerBroadcasting : MonoBehaviour
{
    //We will be using our UdpConnection class to send messages
    private UdpConnection connection;

    //This will decide how often we send a broadcast messages
    //We found that sending a broadcast message once every 2 seconds worked well
    //you don't want to flood the network with broadcast packets
    public float perSecond = .5f;
    private float nextTime = 0;

    //We will pull in game and broadcast information through ClientServerInfo
    public ClientServerInfo ClientServerInfo;
 
    void Start()
    {
        //We only need to run this if we are the server, otherwise disable
        if (!ClientServerInfo.IsServer)
        {
            this.enabled = false;         
        }

        //Get the broadcasting address and port from ClientServerInfo
        string sendToIp = ClientServerInfo.BroadcastIpAddress;
        int sendToPort = ClientServerInfo.BroadcastPort;
 
        //First we create our class
        connection = new UdpConnection();
        //Then we run the initialization method and provide the broadcast IP address and port
        connection.StartConnection(sendToIp, sendToPort);
    }
 
    void Update()
    {
        //We check if it is time to send another broadcast
        if (Time.time >= nextTime)
        {
            //If it is we provide the Send method the game name and time
            //These will be bundled with the server's IP address (which is generated in StartConnection)
            //to be included in the broadcast packet
            connection.Send(nextTime, ClientServerInfo.GameName);
            nextTime += (1/perSecond);
        }
    }

    void OnDestroy()
    {
        //If the server destroys this scene (by returning to NavigationScene) we will call the clean up method
        connection.Stop();
    }
}
  • Now select GameBroadcasting in Hierarchy, and drag the ClientServerInfo GameObject (also in Hierarchy) into the appropriate field in the GameServerBroadcasting component in Inspector

Now the server can send broadcast messages when in MainScene

  • We created a new GameBroadcasting GameObject in MainScene

  • We added GameServerBroadcasting component

Listening and joining on the client

We are going to need to listen for the broadcast messages in NavigationScene. Instead of having LocalGamesFinder search for GameObjects, we will update LocalGamesFinder to listen for broadcast messages.

We will then need to update what happens when we click on the list item, so we need to populate JoinGameScreen with the data sent in the broadcast packet.

  • Navigate to NavigationScene and then open LocalGamesFinder

  • Paste the code snippet below into LocalGamesFinder.cs:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;

public class LocalGamesFinder : MonoBehaviour
{
    //We will be pulling in our SourceAsset from TitleScreenUI GameObject so we can reference Visual Elements
    public UIDocument m_TitleUIDocument;

    //When we grab the rootVisualElement of our UIDocument we will be able to query the TitleScreenManager Visual Element
    private VisualElement m_titleScreenManagerVE;

    //We will query for our TitleScreenManager cVE by its name "TitleScreenManager"
    private TitleScreenManager m_titleScreenManagerClass;

    //Within TitleScreenManager (which is everything) we will query for our list-view by name
    //We don't have to query for the TitleScreen THEN list-view because it is one big tree of elements
    //We can call any child from the parent, very convenient! But you must be mindful about being dilligent about
    //creating unique names or else you can get back several elements (which at times is the point of sharing a name)
    private ListView m_ListView;

    //This is where we will store our received broadcast messages
    private List<ServerInfoObject> discoveredServerInfoObjects = new List<ServerInfoObject>();

    //This is our ListItem uxml that we will drag to the public field
    //We need a reference to the uxml so we can build it in makeItem
    public VisualTreeAsset m_localGameListItemAsset;

    //These variables are used in Update() to pace how often we check for GameObjects
    public float perSecond = 1.0f;
    private float nextTime = 0; 

    ///The broadcast ip address and port to be used by the server across the LAN
    public string BroadcastIpAddress = "255.255.255.255";
    public ushort BroadcastPort = 8014;

    //We will be storing our UdpConnection class as connection
    private UdpConnection connection;

    void OnEnable()
    {
        //Here we grab the SourceAsset rootVisualElement
        //This is a MAJOR KEY, really couldn't find this key step in information online
        //If you want to reference your active UI in a script make a public UIDocument variable and 
        //then call rootVisualElement on it, from there you can query the Visual Element tree by names
        //or element types
        m_titleScreenManagerVE = m_TitleUIDocument.rootVisualElement;
        //Here we grab the TitleScreenManager by querying by name
        m_titleScreenManagerClass = m_titleScreenManagerVE.Q<TitleScreenManager>("TitleScreenManager");
        //From within TitleScreenManager we query local-games-list by name
        m_ListView = m_titleScreenManagerVE.Q<ListView>("local-games-list");

    }

    // Start is called before the first frame update
    void Start()
    {
        //First we pull our broadcast IP address and port from our source of truth, which is this component
        //We actually don't need the broadcast IP address when listening but we provide it anyway because the method
        //requires both arguments (we could provide any IP address and it wouldn't matter, only the server needs to provide the right one)
        string broadcastIp = BroadcastIpAddress;
        //This is the port we will be listening on (this has to be the same as the port the server is sending on)
        int receivePort = BroadcastPort;
 
        //Next we create our UdpConnection class
        connection = new UdpConnection();
        //We provide our broadcast IP adress and port
        connection.StartConnection(broadcastIp, receivePort);
        //Then we start our receive thread which means "listen"
        //This will start creating our queue of received messages that we will call in update
        connection.StartReceiveThread();
        
        
        // The three spells you must cast to conjure a list view
        m_ListView.makeItem = MakeItem;
        m_ListView.bindItem = BindItem;
        m_ListView.itemsSource = discoveredServerInfoObjects;

    }

    private VisualElement MakeItem()
    {
        //Here we take the uxml and make a VisualElement
        VisualElement listItem = m_localGameListItemAsset.CloneTree();
        return listItem;

    }

    private void BindItem(VisualElement e, int index)
    {
        //We add the game name to the label of the list item
        e.Q<Label>("game-name").text = discoveredServerInfoObjects[index].gameName;

        //Here we create a call back for clicking on the list item and provide data to a function
        e.Q<Button>("join-local-game").RegisterCallback<ClickEvent>(ev => ClickedJoinGame(discoveredServerInfoObjects[index]));

    }

    void ClickedJoinGame(ServerInfoObject localGame)
    {
        //We query our JoinGameScreen cVE and call a new function LoadJoinScreenForSelectedServer and pass our GameObject
        //This is an example of clicking a list item and passing through data to a new function with that click
        //You will see in our JoinGameScreen cVE that we use this data to fill labels in the view
        m_titleScreenManagerClass.Q<JoinGameScreen>("JoinGameScreen").LoadJoinScreenForSelectedServer(localGame);

        //We then call EnableJoinScreen on our TitleScreenManager cVE (which displays JoinGameScreen)
        m_titleScreenManagerClass.EnableJoinScreen();

    }
  
    // Update is called once per frame
    void Update()
    {
        if (Time.time >= nextTime)
        {   
            //We grab our array of ServerInfoObjects from our UdpConnection class
            foreach (ServerInfoObject serverInfo in connection.getMessages())
            {
                //We call ReceivedServerInfo so we can check if this ServerInfoObject contains new information
                //We don't use it immediatly and add it to our list because it might already be in the list
                ReceivedServerInfo(serverInfo);
            }
            //We increment
            nextTime += (1/perSecond);
        }
    }

    void ReceivedServerInfo(ServerInfoObject serverInfo)
    {
        //Filter to see if this ServerInfoObject matches with previous broadcasts
        //We will start by thinking that it does not exist
        bool ipExists = false;

        foreach (ServerInfoObject discoveredInfo in discoveredServerInfoObjects)
        {
            //Check if this discovered ip address is already known
            if (serverInfo.ipAddress == discoveredInfo.ipAddress)
            {
                ipExists = true;

                //If a ServerInfoObject with this IP address has been discovered, when did we hear about it?
                float receivedTime = float.Parse(serverInfo.timeStamp);
                //What about this broadcast?
                float storedTime = float.Parse(discoveredInfo.timeStamp);

                //We will update to the latest information from the IP address that has been broadcast
                //The host might have quit and started a new game and we want to display the latest info
                if (receivedTime > storedTime)
                {
                    //Set the data to the new data
                    discoveredInfo.gameName = serverInfo.gameName;
                    discoveredInfo.timeStamp = serverInfo.timeStamp;
                    //Now we need to update the table
                    m_ListView.Refresh();
                }
            }

        }
        //If the ip didn't already exist, add it to the known list
        if (!ipExists)
        {
            //We add it to the list
            discoveredServerInfoObjects.Add(serverInfo);
            //We refresh our list to display the new data
            m_ListView.Refresh();
        }
    }

    //We must call the clean up function on UdpConnection or else the thread will keep running!
    void OnDestroy()
    {
        connection.Stop();
    }
}
  • Now that we have updated LocalGamesFinder to update the list with the latest unique broadcasts we need to update JoinGameScreen custom Visual Element (cVE) to take in the new passed-through ServerInfoObject data

  • Paste the code snippet below into JoinGameScreen.cs (cVE):

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.Collections;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine.SceneManagement;

public class JoinGameScreen : VisualElement
{
    Label m_GameName;
    Label m_GameIp;
    TextField m_PlayerName;
    String m_HostName = "";
    IPAddress m_MyIp;

    public new class UxmlFactory : UxmlFactory<JoinGameScreen, UxmlTraits> { }

    public JoinGameScreen()
    {
        this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
    }

    void OnGeometryChange(GeometryChangedEvent evt)
    {
        // 
        // PROVIDE ACCESS TO THE FORM ELEMENTS THROUGH VARIABLES
        // 
        m_GameName = this.Q<Label>("game-name");
        m_GameIp = this.Q<Label>("game-ip");
        m_PlayerName = this.Q<TextField>("player-name");

        //Grab the system name
        m_HostName = Dns.GetHostName();
        //Set the value equal to the host name to start
        m_PlayerName.value = m_HostName;

        this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
    }

    public void LoadJoinScreenForSelectedServer(ServerInfoObject localGame)
    {

        m_GameName = this.Q<Label>("game-name");
        m_GameIp = this.Q<Label>("game-ip");
        m_GameName.text = localGame.gameName;
        m_GameIp.text = localGame.ipAddress;
    }
}
  • Now delete all the LocalGame GameObjects in the NavigationScene Hierarchy

  • Save the NavigationScene

  • Navigate to the BuildSettings folder, select the configuration file of your development platform (i.e. "macOS - Build"), then go to Inspector and hit Build and Run

  • Change the port in line 47 in UdpConnection to 9001

    • We make this change so that we don't bind on same port when testing

try { udpClient = new UdpClient(9001); }
  • Hit play in the editor

  • Host a game in the editor and check out the build

    • Remember to host in the editor, not the build, when testing

    • The editor must send to the hardwired broadcast port

  • Great, we see our broadcasted game

  • In the editor: quit the game, and then start a new game with a new name

  • We can see our game name change in our build

  • In the build: click on the broadcasted game and join the game

  • Undo the port change in UdpConnection and we are set!

    • If you want to test again you will need to build then change like we did in this section

We are now able to join broadcasted games

  • We updated LocalGamesFinder

  • We updated JoinGameScreen cVE

  • We updated the port in UdpConnection for testing

  • We built our project and was able to broadcast and join a game

Github branch link:

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Broadcasting-and-Joining-on-LAN'

Last updated