diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a699eb4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2019 in0finite + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/NetworkDiscovery.cs b/NetworkDiscovery.cs new file mode 100644 index 0000000..4622974 --- /dev/null +++ b/NetworkDiscovery.cs @@ -0,0 +1,585 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Net.NetworkInformation; +using System.Runtime.Serialization.Formatters.Binary; +using UnityEngine.Profiling; + +namespace Mirror +{ + + public class NetworkDiscovery : MonoBehaviour + { + + public class DiscoveryInfo + { + private IPEndPoint endPoint; + private Dictionary keyValuePairs = new Dictionary (); + private float timeWhenReceived = 0f; + public DiscoveryInfo (IPEndPoint endPoint, Dictionary keyValuePairs) + { + this.endPoint = endPoint; + this.keyValuePairs = keyValuePairs; + this.timeWhenReceived = Time.realtimeSinceStartup; + } + public IPEndPoint EndPoint { get { return this.endPoint; } } + public Dictionary KeyValuePairs { get { return this.keyValuePairs; } } + public float TimeSinceReceived { get { return Time.realtimeSinceStartup - this.timeWhenReceived; } } + } + + public static event System.Action onReceivedServerResponse = delegate {}; + + // server sends this data as a response to broadcast + static Dictionary m_responseData = new Dictionary (); + + public static NetworkDiscovery singleton { get ; private set ; } + + public const string kSignatureKey = "Signature", kPortKey = "Port", kNumPlayersKey = "Players", + kMaxNumPlayersKey = "MaxNumPlayers", kMapNameKey = "Map"; + + public const int kDefaultServerPort = 18418; + + // [SerializeField] int m_clientPort = 18417; + [SerializeField] int m_serverPort = kDefaultServerPort; + static UdpClient m_serverUdpCl = null; + static UdpClient m_clientUdpCl = null; + + static string m_signature = null; + + static bool m_wasServerActiveLastTime = false; + + public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } } + + static bool IsServerActive { get { return NetworkServer.active; } } + // static bool IsClientActive { get { return NetworkClient.active; } } + public int gameServerPortNumber = 7777; + static int NumPlayers { get { return NetworkServer.connections.Count; } } + static int MaxNumPlayers { get { return NetworkManager.singleton != null ? NetworkManager.singleton.maxConnections : 0; } } + + + + void Awake () + { + if (singleton != null) + return; + + singleton = this; + + } + + void Start () + { + if(!SupportedOnThisPlatform) + return; + + StartCoroutine (ClientCoroutine ()); + + StartCoroutine (ServerCoroutine ()); + + } + + void OnDisable () + { + ShutdownUdpClients (); + } + + + void Update () + { + if (!SupportedOnThisPlatform) + return; + + if (IsServerActive) + { + UpdateResponseData (); + } + + bool isServerActiveNow = IsServerActive; + + if (isServerActiveNow != m_wasServerActiveLastTime) + { + // server status changed + // start/stop server's udp client + + m_wasServerActiveLastTime = isServerActiveNow; + + if (isServerActiveNow) + EnsureServerIsInitialized(); + else + CloseServerUdpClient(); + + } + + } + + + static void EnsureServerIsInitialized() + { + + if (m_serverUdpCl != null) + return; + + m_serverUdpCl = new UdpClient (singleton.m_serverPort); + RunSafe( () => { m_serverUdpCl.EnableBroadcast = true; } ); + RunSafe( () => { m_serverUdpCl.MulticastLoopback = false; } ); + + // m_serverUdpCl.BeginReceive(new System.AsyncCallback(ReceiveCallback), null); + + } + + static void EnsureClientIsInitialized() + { + + if (m_clientUdpCl != null) + return; + + m_clientUdpCl = new UdpClient (0); + RunSafe( () => { m_clientUdpCl.EnableBroadcast = true; } ); + // turn off receiving from our IP + RunSafe( () => { m_clientUdpCl.MulticastLoopback = false; } ); + + } + + static void ShutdownUdpClients() + { + CloseServerUdpClient(); + CloseClientUdpClient(); + } + + static void CloseServerUdpClient() + { + if (m_serverUdpCl != null) { + m_serverUdpCl.Close (); + m_serverUdpCl = null; + } + } + + static void CloseClientUdpClient() + { + if (m_clientUdpCl != null) { + m_clientUdpCl.Close (); + m_clientUdpCl = null; + } + } + + + static DiscoveryInfo ReadDataFromUdpClient(UdpClient udpClient) + { + + // only proceed if there is available data in network buffer, or otherwise Receive() will block + // average time for UdpClient.Available : 10 us + if (udpClient.Available <= 0) + return null; + + Profiler.BeginSample("UdpClient.Receive"); + IPEndPoint remoteEP = new IPEndPoint (IPAddress.Any, 0); + byte[] receivedBytes = udpClient.Receive (ref remoteEP); + Profiler.EndSample (); + + if (remoteEP != null && receivedBytes != null && receivedBytes.Length > 0) { + + Profiler.BeginSample ("Convert data"); + var dict = ConvertByteArrayToDictionary (receivedBytes); + Profiler.EndSample (); + + return new DiscoveryInfo(remoteEP, dict); + } + + return null; + } + + static System.Collections.IEnumerator ServerCoroutine() + { + + while (true) + { + + yield return null; + + if (null == m_serverUdpCl) + continue; + + if(!IsServerActive) + continue; + + // average time for this (including data receiving and processing): less than 100 us + Profiler.BeginSample ("Receive broadcast"); + // var timer = System.Diagnostics.Stopwatch.StartNew (); + + RunSafe (() => + { + var info = ReadDataFromUdpClient(m_serverUdpCl); + if(info != null) + OnReceivedBroadcast(info); + }); + + // Debug.Log("receive broadcast time: " + timer.GetElapsedMicroSeconds () + " us"); + Profiler.EndSample (); + + } + + } + + static void OnReceivedBroadcast(DiscoveryInfo info) + { + if(info.KeyValuePairs.ContainsKey(kSignatureKey) && info.KeyValuePairs[kSignatureKey] == GetSignature()) + { + // signature matches + // send response + + Profiler.BeginSample("Send response"); + byte[] bytes = ConvertDictionaryToByteArray( m_responseData ); + m_serverUdpCl.Send( bytes, bytes.Length, info.EndPoint ); + Profiler.EndSample(); + } + } + + static System.Collections.IEnumerator ClientCoroutine() + { + + while (true) + { + yield return null; + + if (null == m_clientUdpCl) + continue; + + RunSafe( () => + { + var info = ReadDataFromUdpClient (m_clientUdpCl); + if (info != null) + OnReceivedServerResponse(info); + }); + + } + + } + + + public static byte[] GetDiscoveryRequestData() + { + Profiler.BeginSample("ConvertDictionaryToByteArray"); + var dict = new Dictionary() {{kSignatureKey, GetSignature()}}; + byte[] buffer = ConvertDictionaryToByteArray (dict); + Profiler.EndSample(); + + return buffer; + } + + public static void SendBroadcast() + { + if (!SupportedOnThisPlatform) + return; + + byte[] buffer = GetDiscoveryRequestData(); + + // We can't just send packet to 255.255.255.255 - the OS will only broadcast it to the network interface + // which the socket is bound to. + // We need to broadcast packet on every network interface. + + IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, singleton.m_serverPort); + + foreach(var address in GetBroadcastAdresses()) + { + endPoint.Address = address; + SendDiscoveryRequest(endPoint, buffer); + } + + } + + public static void SendDiscoveryRequest(IPEndPoint endPoint) + { + SendDiscoveryRequest(endPoint, GetDiscoveryRequestData()); + } + + static void SendDiscoveryRequest(IPEndPoint endPoint, byte[] buffer) + { + if (!SupportedOnThisPlatform) + return; + + EnsureClientIsInitialized(); + + if (null == m_clientUdpCl) + return; + + + Profiler.BeginSample("UdpClient.Send"); + try { + m_clientUdpCl.Send (buffer, buffer.Length, endPoint); + } catch(SocketException ex) { + if(ex.ErrorCode == 10051) { + // Network is unreachable + // ignore this error + + } else { + throw; + } + } + Profiler.EndSample(); + + } + + + public static IPAddress[] GetBroadcastAdresses() + { + // try multiple methods - because some of them may fail on some devices, especially if IL2CPP comes into play + + IPAddress[] ips = null; + + RunSafe(() => ips = GetBroadcastAdressesFromNetworkInterfaces(), false); + + if (null == ips || ips.Length < 1) + { + // try another method + RunSafe(() => ips = GetBroadcastAdressesFromHostEntry(), false); + } + + if (null == ips || ips.Length < 1) + { + // all methods failed, or there is no network interface on this device + // just use full-broadcast address + ips = new IPAddress[]{IPAddress.Broadcast}; + } + + return ips; + } + + static IPAddress[] GetBroadcastAdressesFromNetworkInterfaces() + { + List ips = new List(); + + var nifs = NetworkInterface.GetAllNetworkInterfaces() + .Where(nif => nif.OperationalStatus == OperationalStatus.Up) + .Where(nif => nif.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 || nif.NetworkInterfaceType == NetworkInterfaceType.Ethernet); + + foreach (var nif in nifs) + { + foreach (UnicastIPAddressInformation ipInfo in nif.GetIPProperties().UnicastAddresses) + { + var ip = ipInfo.Address; + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + if(ToBroadcastAddress(ref ip, ipInfo.IPv4Mask)) + ips.Add(ip); + } + } + } + + return ips.ToArray(); + } + + static IPAddress[] GetBroadcastAdressesFromHostEntry() + { + var ips = new List (); + + IPHostEntry hostEntry = Dns.GetHostEntry(Dns.GetHostName()); + + foreach(var address in hostEntry.AddressList) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + { + // this is IPv4 address + // convert it to broadcast address + // use default subnet + + var subnetMask = GetDefaultSubnetMask(address); + if (subnetMask != null) + { + var broadcastAddress = address; + if (ToBroadcastAddress(ref broadcastAddress, subnetMask)) + ips.Add( broadcastAddress ); + } + } + } + + if (ips.Count > 0) + { + // if we found at least 1 ip, then also add full-broadcast address + // this will compensate in case we used a wrong subnet mask + ips.Add(IPAddress.Broadcast); + } + + return ips.ToArray(); + } + + static bool ToBroadcastAddress(ref IPAddress ip, IPAddress subnetMask) + { + if (ip.AddressFamily != AddressFamily.InterNetwork || subnetMask.AddressFamily != AddressFamily.InterNetwork) + return false; + + byte[] bytes = ip.GetAddressBytes(); + byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + + for(int i=0; i < 4; i++) + { + // on places where subnet mask has 1s, address bits are copied, + // and on places where subnet mask has 0s, address bits are 1 + bytes[i] = (byte) ((~subnetMaskBytes[i]) | bytes[i]); + } + + ip = new IPAddress(bytes); + + return true; + } + + static IPAddress GetDefaultSubnetMask(IPAddress ip) + { + if (ip.AddressFamily != AddressFamily.InterNetwork) + return null; + + IPAddress subnetMask; + + byte[] bytes = ip.GetAddressBytes(); + byte firstByte = bytes[0]; + + if (firstByte >= 0 && firstByte <= 127) + subnetMask = new IPAddress(new byte[]{255, 0, 0, 0}); + else if (firstByte >= 128 && firstByte <= 191) + subnetMask = new IPAddress(new byte[]{255, 255, 0, 0}); + else if (firstByte >= 192 && firstByte <= 223) + subnetMask = new IPAddress(new byte[]{255, 255, 255, 0}); + else // undefined subnet + subnetMask = null; + + return subnetMask; + } + + + static void OnReceivedServerResponse(DiscoveryInfo info) { + + // check if data is valid + if(!IsDataFromServerValid(info)) + return; + + // invoke event + onReceivedServerResponse(info); + + } + + public static bool IsDataFromServerValid(DiscoveryInfo data) + { + // data must contain signature which matches, and port number + return data.KeyValuePairs.ContainsKey(kSignatureKey) && data.KeyValuePairs[kSignatureKey] == GetSignature() + && data.KeyValuePairs.ContainsKey(kPortKey); + } + + + public static void RegisterResponseData( string key, string value ) + { + m_responseData[key] = value; + } + + public static void UnRegisterResponseData( string key ) + { + m_responseData.Remove (key); + } + + /// + /// Adds/updates some default response data. + /// + public static void UpdateResponseData() + { + + RegisterResponseData (kSignatureKey, GetSignature()); + RegisterResponseData (kPortKey, singleton.gameServerPortNumber.ToString()); + RegisterResponseData (kNumPlayersKey, NumPlayers.ToString ()); + RegisterResponseData (kMaxNumPlayersKey, MaxNumPlayers.ToString ()); + RegisterResponseData (kMapNameKey, UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); + + } + + /// Signature identifies this game among others. + public static string GetSignature() + { + if (m_signature != null) + return m_signature; + + string[] strings = new string[]{ Application.companyName, Application.productName, + Application.unityVersion }; + + m_signature = ""; + + foreach(string str in strings) + { + // only use it's hash code + m_signature += str.GetHashCode() + "."; + } + + return m_signature; + } + + + public static string ConvertDictionaryToString( Dictionary dict ) + { + return string.Join( "\n", dict.Select( pair => pair.Key + ": " + pair.Value ) ); + } + + public static Dictionary ConvertStringToDictionary( string str ) + { + var dict = new Dictionary(); + string[] lines = str.Split("\n".ToCharArray(), System.StringSplitOptions.RemoveEmptyEntries); + foreach(string line in lines) + { + int index = line.IndexOf(": "); + if(index < 0) + continue; + dict[line.Substring(0, index)] = line.Substring(index + 2, line.Length - index - 2); + } + return dict; + } + + public static byte[] ConvertDictionaryToByteArray( Dictionary dict ) + { + return ConvertStringToPacketData( ConvertDictionaryToString( dict ) ); + } + + public static Dictionary ConvertByteArrayToDictionary( byte[] data ) + { + return ConvertStringToDictionary( ConvertPacketDataToString( data ) ); + } + + public static byte[] ConvertStringToPacketData(string str) + { + byte[] data = new byte[str.Length * 2]; + for (int i = 0; i < str.Length; i++) + { + ushort c = str[i]; + data[i * 2] = (byte) ((c & 0xff00) >> 8); + data[i * 2 + 1] = (byte) (c & 0x00ff); + } + return data; + } + + public static string ConvertPacketDataToString(byte[] data) + { + char[] arr = new char[data.Length / 2]; + for (int i = 0; i < arr.Length; i++) + { + ushort b1 = data[i * 2]; + ushort b2 = data[i * 2 + 1]; + arr[i] = (char)((b1 << 8) | b2); + } + return new string(arr); + } + + + static bool RunSafe(System.Action action, bool logException = true) + { + try + { + action(); + return true; + } + catch(System.Exception ex) + { + if (logException) + Debug.LogException(ex); + return false; + } + } + + } + +} diff --git a/NetworkDiscoveryHUD.cs b/NetworkDiscoveryHUD.cs new file mode 100644 index 0000000..b3953a0 --- /dev/null +++ b/NetworkDiscoveryHUD.cs @@ -0,0 +1,253 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.Linq; +using System.Net; + +namespace Mirror +{ + + public class NetworkDiscoveryHUD : MonoBehaviour + { + List m_discoveredServers = new List(); + string[] m_headerNames = new string[]{"IP", NetworkDiscovery.kMapNameKey, NetworkDiscovery.kNumPlayersKey, + NetworkDiscovery.kMaxNumPlayersKey}; + Vector2 m_scrollViewPos = Vector2.zero; + public bool IsRefreshing { get { return Time.realtimeSinceStartup - m_timeWhenRefreshed < this.refreshInterval; } } + float m_timeWhenRefreshed = 0f; + bool m_displayBroadcastAddresses = false; + + IPEndPoint m_lookupServer = null; // server that we are currently looking up + string m_lookupServerIP = ""; + string m_lookupServerPort = NetworkDiscovery.kDefaultServerPort.ToString(); + float m_timeWhenLookedUpServer = 0f; + bool IsLookingUpAnyServer { get { return Time.realtimeSinceStartup - m_timeWhenLookedUpServer < this.refreshInterval + && m_lookupServer != null; } } + + GUIStyle m_centeredLabelStyle; + + public bool drawGUI = true; + public int offsetX = 5; + public int offsetY = 150; + public int width = 500, height = 400; + [Range(1, 5)] public float refreshInterval = 3f; + + public System.Action connectAction; + + + + NetworkDiscoveryHUD() + { + this.connectAction = this.Connect; + } + + void OnEnable() + { + NetworkDiscovery.onReceivedServerResponse += OnDiscoveredServer; + } + + void OnDisable() + { + NetworkDiscovery.onReceivedServerResponse -= OnDiscoveredServer; + } + + void Start() + { + + } + + void OnGUI() + { + + if (null == m_centeredLabelStyle) + { + m_centeredLabelStyle = new GUIStyle(GUI.skin.label); + m_centeredLabelStyle.alignment = TextAnchor.MiddleCenter; + } + + if (this.drawGUI) + this.Display(new Rect(offsetX, offsetY, width, height)); + + } + + public void Display(Rect displayRect) + { + if (null == NetworkManager.singleton) + return; + if (NetworkServer.active || NetworkClient.active) + return; + if (!NetworkDiscovery.SupportedOnThisPlatform) + return; + + GUILayout.BeginArea(displayRect); + + this.DisplayRefreshButton(); + + // lookup a server + + GUILayout.Label("Lookup server: "); + GUILayout.BeginHorizontal(); + GUILayout.Label("IP:"); + m_lookupServerIP = GUILayout.TextField(m_lookupServerIP, GUILayout.Width(120)); + GUILayout.Space(10); + GUILayout.Label("Port:"); + m_lookupServerPort = GUILayout.TextField(m_lookupServerPort, GUILayout.Width(60)); + GUILayout.Space(10); + if (IsLookingUpAnyServer) + { + GUILayout.Button("Lookup...", GUILayout.Height(25), GUILayout.MinWidth(80)); + } + else + { + if (GUILayout.Button("Lookup", GUILayout.Height(25), GUILayout.MinWidth(80))) + LookupServer(); + } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + m_displayBroadcastAddresses = GUILayout.Toggle(m_displayBroadcastAddresses, "Display broadcast addresses", GUILayout.ExpandWidth(false)); + if (m_displayBroadcastAddresses) + { + GUILayout.Space(10); + GUILayout.Label( string.Join( ", ", NetworkDiscovery.GetBroadcastAdresses().Select(ip => ip.ToString()) ) ); + } + GUILayout.EndHorizontal(); + + GUILayout.Label(string.Format("Servers [{0}]:", m_discoveredServers.Count)); + + this.DisplayServers(); + + GUILayout.EndArea(); + + } + + public void DisplayRefreshButton() + { + if(IsRefreshing) + { + GUILayout.Button("Refreshing...", GUILayout.Height(25), GUILayout.ExpandWidth(false)); + } + else + { + if (GUILayout.Button("Refresh LAN", GUILayout.Height(25), GUILayout.ExpandWidth(false))) + { + Refresh(); + } + } + } + + public void DisplayServers() + { + + int elemWidth = this.width / m_headerNames.Length - 5; + + // header + GUILayout.BeginHorizontal(); + foreach(string str in m_headerNames) + GUILayout.Button(str, GUILayout.Width(elemWidth)); + GUILayout.EndHorizontal(); + + // servers + + m_scrollViewPos = GUILayout.BeginScrollView(m_scrollViewPos); + + foreach(var info in m_discoveredServers) + { + GUILayout.BeginHorizontal(); + + if( GUILayout.Button(info.EndPoint.Address.ToString(), GUILayout.Width(elemWidth)) ) + this.connectAction(info); + + for( int i = 1; i < m_headerNames.Length; i++ ) + { + if (info.KeyValuePairs.ContainsKey(m_headerNames[i])) + GUILayout.Label(info.KeyValuePairs[m_headerNames[i]], m_centeredLabelStyle, GUILayout.Width(elemWidth)); + else + GUILayout.Label("", m_centeredLabelStyle, GUILayout.Width(elemWidth)); + } + + GUILayout.EndHorizontal(); + } + + GUILayout.EndScrollView(); + + } + + public void Refresh() + { + m_discoveredServers.Clear(); + + m_timeWhenRefreshed = Time.realtimeSinceStartup; + + NetworkDiscovery.SendBroadcast(); + + } + + public void LookupServer() + { + // parse IP and port + + IPAddress ip = IPAddress.Parse(m_lookupServerIP); + ushort port = ushort.Parse(m_lookupServerPort); + + // input is ok + // send discovery request + + m_timeWhenLookedUpServer = Time.realtimeSinceStartup; + + m_lookupServer = new IPEndPoint(ip, port); + + NetworkDiscovery.SendDiscoveryRequest(m_lookupServer); + } + + bool IsLookingUpServer(IPEndPoint endPoint) + { + return Time.realtimeSinceStartup - m_timeWhenLookedUpServer < this.refreshInterval + && m_lookupServer != null + && m_lookupServer.Equals(endPoint); + } + + void Connect(NetworkDiscovery.DiscoveryInfo info) + { + if (null == NetworkManager.singleton) + return; + if (null == Transport.activeTransport) + return; + if (!(Transport.activeTransport is TelepathyTransport)) + { + Debug.LogErrorFormat("Only {0} is supported", typeof(TelepathyTransport)); + return; + } + + // assign address and port + NetworkManager.singleton.networkAddress = info.EndPoint.Address.ToString(); + ((TelepathyTransport) Transport.activeTransport).port = ushort.Parse( info.KeyValuePairs[NetworkDiscovery.kPortKey] ); + + NetworkManager.singleton.StartClient(); + } + + void OnDiscoveredServer(NetworkDiscovery.DiscoveryInfo info) + { + if (!IsRefreshing && !IsLookingUpServer(info.EndPoint)) + return; + + int index = m_discoveredServers.FindIndex(item => item.EndPoint.Equals(info.EndPoint)); + if(index < 0) + { + // server is not in the list + // add it + m_discoveredServers.Add(info); + } + else + { + // server is in the list + // update it + m_discoveredServers[index] = info; + } + + } + + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f710ed --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ + +## NetworkDiscoveryUnity + +Network discovery for Unity3D. + + +## Features + +- Simple. 1 script. 600 lines of code. + +- Uses C#'s UDP sockets for broadcasting and sending responses. + +- Independent of current networking framework. + +- Single-threaded. + +- Tested on: Linux, Windows, Android. + +- Can lookup specific servers on the internet (outside of local network). + +- Has a separate [GUI script](/NetworkDiscoveryHUD.cs) for easy testing. + +- Has support for custom response data. + +- By default, server responds with: current scene, game server port number, game signature. + +- No impact on performance. + + +## Usage + +Attach [NetworkDiscovery](/NetworkDiscovery.cs) script to any game object. Assign game server port number. + +Now, you can use [NetworkDiscoveryHUD](/NetworkDiscoveryHUD.cs) script to test it (by attaching it to the same game object), or use the API directly: + +```cs +// register listener +NetworkDiscovery.onReceivedServerResponse += (NetworkDiscovery.DiscoveryInfo info) => +{ + // we received response from server + // add it to list of servers, or connect immediately... +}; + +// send broadcast on LAN +// when server receives the packet, he will respond +NetworkDiscovery.SendBroadcast(); + +// on server side, you can register custom data for responding +NetworkDiscovery.RegisterResponseData("Game mode", "Deathmatch"); +``` + +For more details on how to use it, check out NetworkDiscoveryHUD script. + + +## Inspector + +![](https://i.imgur.com/R9ZU1G2.png) + + +## Example GUI + +![](https://i.imgur.com/SXqKMbJ.png) + + +## Possible improvements + +- Measure ping - requires that all socket operations are done in a separate thread, or using async methods + +- Prevent detection of multiple localhost servers (by assigning GUID to each packet) ? + +- Add "Refresh" button in GUI next to each server + +- Catch the other exception which is thrown on windows - it's harmless, so it should not be logged + +- Make sure packet-to-string conversion works with non-ascii characters +