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 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