├── LICENSE ├── LICENSE.meta ├── NetworkDiscovery.cs ├── NetworkDiscovery.cs.meta ├── NetworkDiscoveryHUD.cs ├── NetworkDiscoveryHUD.cs.meta ├── NetworkDiscoveryUnity.asmdef ├── NetworkDiscoveryUnity.asmdef.meta ├── README.md └── README.md.meta /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 in0finite 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c49f4dd907f98b14f956555e81a5926a 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /NetworkDiscovery.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Net.NetworkInformation; 7 | using UnityEngine.Profiling; 8 | using UnityEngine.SceneManagement; 9 | using System.Globalization; 10 | 11 | namespace NetworkDiscoveryUnity 12 | { 13 | 14 | public class NetworkDiscovery : MonoBehaviour 15 | { 16 | 17 | public class DiscoveryInfo 18 | { 19 | public readonly IPEndPoint EndPoint; 20 | public readonly IReadOnlyDictionary KeyValuePairs; 21 | private float m_timeWhenReceived = 0f; 22 | public float TimeSinceReceived => Time.realtimeSinceStartup - m_timeWhenReceived; 23 | 24 | public DiscoveryInfo (IPEndPoint endPoint, Dictionary keyValuePairs) 25 | { 26 | this.EndPoint = endPoint; 27 | this.KeyValuePairs = keyValuePairs; 28 | m_timeWhenReceived = Time.realtimeSinceStartup; 29 | } 30 | 31 | public ushort GetGameServerPort() => ushort.Parse(this.KeyValuePairs[kPortKey], CultureInfo.InvariantCulture); 32 | 33 | public bool TryGetGameServerPort(out ushort port) 34 | { 35 | port = 0; 36 | return this.KeyValuePairs.TryGetValue(kPortKey, out string portString) 37 | && ushort.TryParse(portString, NumberStyles.None, CultureInfo.InvariantCulture, out port); 38 | } 39 | } 40 | 41 | public UnityEngine.Events.UnityEvent onReceivedServerResponse = 42 | new UnityEngine.Events.UnityEvent(); 43 | 44 | // server sends this data as a response to broadcast 45 | readonly Dictionary m_responseData = 46 | new Dictionary (System.StringComparer.InvariantCulture); 47 | 48 | public static NetworkDiscovery Instance { get ; private set ; } 49 | 50 | public const string kSignatureKey = "Signature", kPortKey = "Port", kMapNameKey = "Map"; 51 | 52 | public const int kDefaultServerPort = 18418; 53 | 54 | [SerializeField] int m_serverPort = kDefaultServerPort; 55 | UdpClient m_serverUdpCl = null; 56 | UdpClient m_clientUdpCl = null; 57 | 58 | public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } } 59 | 60 | public int gameServerPortNumber = 7777; 61 | 62 | private static string s_cachedSignature = null; 63 | 64 | 65 | 66 | void Awake () 67 | { 68 | if (null == Instance) 69 | Instance = this; 70 | 71 | RegisterResponseData(kSignatureKey, GetCachedSignature()); 72 | RegisterResponseData(kPortKey, this.gameServerPortNumber.ToString(CultureInfo.InvariantCulture)); 73 | RegisterResponseData(kMapNameKey, SceneManager.GetActiveScene().name); 74 | } 75 | 76 | void OnEnable() 77 | { 78 | SceneManager.activeSceneChanged += OnActiveSceneChanged; 79 | } 80 | 81 | void OnDisable () 82 | { 83 | SceneManager.activeSceneChanged -= OnActiveSceneChanged; 84 | ShutdownUdpClients (); 85 | } 86 | 87 | void OnActiveSceneChanged(Scene oldScene, Scene newScene) 88 | { 89 | RegisterResponseData(kMapNameKey, SceneManager.GetActiveScene().name); 90 | } 91 | 92 | void Update() 93 | { 94 | UpdateServer(); 95 | UpdateClient(); 96 | } 97 | 98 | 99 | public void EnsureServerIsInitialized() 100 | { 101 | 102 | if (m_serverUdpCl != null) 103 | return; 104 | 105 | m_serverUdpCl = new UdpClient (m_serverPort); 106 | RunSafe( () => { m_serverUdpCl.EnableBroadcast = true; } ); 107 | RunSafe( () => { m_serverUdpCl.MulticastLoopback = false; } ); 108 | 109 | } 110 | 111 | void EnsureClientIsInitialized() 112 | { 113 | 114 | if (m_clientUdpCl != null) 115 | return; 116 | 117 | m_clientUdpCl = new UdpClient (0); 118 | RunSafe( () => { m_clientUdpCl.EnableBroadcast = true; } ); 119 | // turn off receiving from our IP 120 | RunSafe( () => { m_clientUdpCl.MulticastLoopback = false; } ); 121 | 122 | } 123 | 124 | void ShutdownUdpClients() 125 | { 126 | CloseServerUdpClient(); 127 | CloseClientUdpClient(); 128 | } 129 | 130 | public void CloseServerUdpClient() 131 | { 132 | if (m_serverUdpCl != null) { 133 | m_serverUdpCl.Close (); 134 | m_serverUdpCl = null; 135 | } 136 | } 137 | 138 | void CloseClientUdpClient() 139 | { 140 | if (m_clientUdpCl != null) { 141 | m_clientUdpCl.Close (); 142 | m_clientUdpCl = null; 143 | } 144 | } 145 | 146 | 147 | static DiscoveryInfo ReadDataFromUdpClient(UdpClient udpClient) 148 | { 149 | 150 | // only proceed if there is available data in network buffer, or otherwise Receive() will block 151 | // average time for UdpClient.Available : 10 us 152 | if (udpClient.Available <= 0) 153 | return null; 154 | 155 | Profiler.BeginSample("UdpClient.Receive"); 156 | IPEndPoint remoteEP = new IPEndPoint (IPAddress.Any, 0); 157 | byte[] receivedBytes = udpClient.Receive (ref remoteEP); 158 | Profiler.EndSample (); 159 | 160 | if (remoteEP != null && receivedBytes != null && receivedBytes.Length > 0) { 161 | 162 | Profiler.BeginSample ("Convert data"); 163 | var dict = ConvertByteArrayToDictionary (receivedBytes); 164 | Profiler.EndSample (); 165 | 166 | return new DiscoveryInfo(remoteEP, dict); 167 | } 168 | 169 | return null; 170 | } 171 | 172 | void UpdateServer() 173 | { 174 | if (null == m_serverUdpCl) 175 | return; 176 | 177 | 178 | // average time for this (including data receiving and processing): less than 100 us 179 | Profiler.BeginSample ("Receive broadcast"); 180 | // var timer = System.Diagnostics.Stopwatch.StartNew (); 181 | 182 | var info = ReadDataFromUdpClient(m_serverUdpCl); 183 | if (info != null) 184 | OnReceivedBroadcast(info); 185 | 186 | // Debug.Log("receive broadcast time: " + timer.GetElapsedMicroSeconds () + " us"); 187 | Profiler.EndSample (); 188 | 189 | } 190 | 191 | void OnReceivedBroadcast(DiscoveryInfo info) 192 | { 193 | if(info.KeyValuePairs.ContainsKey(kSignatureKey) && info.KeyValuePairs[kSignatureKey] == GetCachedSignature()) 194 | { 195 | // signature matches 196 | // send response 197 | 198 | Profiler.BeginSample("Send response"); 199 | byte[] bytes = ConvertDictionaryToByteArray( m_responseData ); 200 | m_serverUdpCl.Send( bytes, bytes.Length, info.EndPoint ); 201 | Profiler.EndSample(); 202 | } 203 | } 204 | 205 | void UpdateClient() 206 | { 207 | if (null == m_clientUdpCl) 208 | return; 209 | 210 | var info = ReadDataFromUdpClient(m_clientUdpCl); 211 | if (info != null) 212 | OnReceivedServerResponse(info); 213 | 214 | } 215 | 216 | 217 | public static byte[] GetDiscoveryRequestData() 218 | { 219 | Profiler.BeginSample("ConvertDictionaryToByteArray"); 220 | var dict = new Dictionary(System.StringComparer.InvariantCulture) {{kSignatureKey, GetCachedSignature()}}; 221 | byte[] buffer = ConvertDictionaryToByteArray (dict); 222 | Profiler.EndSample(); 223 | 224 | return buffer; 225 | } 226 | 227 | public void SendBroadcast() 228 | { 229 | if (!SupportedOnThisPlatform) 230 | return; 231 | 232 | byte[] buffer = GetDiscoveryRequestData(); 233 | 234 | // We can't just send packet to 255.255.255.255 - the OS will only broadcast it to the network interface 235 | // which the socket is bound to. 236 | // We need to broadcast packet on every network interface. 237 | 238 | IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, m_serverPort); 239 | 240 | foreach(var address in GetBroadcastAdresses()) 241 | { 242 | endPoint.Address = address; 243 | SendDiscoveryRequest(endPoint, buffer); 244 | } 245 | 246 | } 247 | 248 | public void SendDiscoveryRequest(IPEndPoint endPoint) 249 | { 250 | SendDiscoveryRequest(endPoint, GetDiscoveryRequestData()); 251 | } 252 | 253 | void SendDiscoveryRequest(IPEndPoint endPoint, byte[] buffer) 254 | { 255 | if (!SupportedOnThisPlatform) 256 | return; 257 | 258 | EnsureClientIsInitialized(); 259 | 260 | if (null == m_clientUdpCl) 261 | return; 262 | 263 | 264 | Profiler.BeginSample("UdpClient.Send"); 265 | try { 266 | m_clientUdpCl.Send (buffer, buffer.Length, endPoint); 267 | } catch(SocketException ex) { 268 | if(ex.ErrorCode == 10051) { 269 | // Network is unreachable 270 | // ignore this error 271 | 272 | } else { 273 | throw; 274 | } 275 | } 276 | Profiler.EndSample(); 277 | 278 | } 279 | 280 | 281 | public static IPAddress[] GetBroadcastAdresses() 282 | { 283 | // try multiple methods - because some of them may fail on some devices, especially if IL2CPP comes into play 284 | 285 | IPAddress[] ips = null; 286 | 287 | RunSafe(() => ips = GetBroadcastAdressesFromNetworkInterfaces(), false); 288 | 289 | if (null == ips || ips.Length < 1) 290 | { 291 | // try another method 292 | RunSafe(() => ips = GetBroadcastAdressesFromHostEntry(), false); 293 | } 294 | 295 | if (null == ips || ips.Length < 1) 296 | { 297 | // all methods failed, or there is no network interface on this device 298 | // just use full-broadcast address 299 | ips = new IPAddress[]{IPAddress.Broadcast}; 300 | } 301 | 302 | return ips; 303 | } 304 | 305 | static IPAddress[] GetBroadcastAdressesFromNetworkInterfaces() 306 | { 307 | List ips = new List(); 308 | 309 | var nifs = NetworkInterface.GetAllNetworkInterfaces() 310 | .Where(nif => nif.OperationalStatus == OperationalStatus.Up) 311 | .Where(nif => nif.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 || nif.NetworkInterfaceType == NetworkInterfaceType.Ethernet); 312 | 313 | foreach (var nif in nifs) 314 | { 315 | foreach (UnicastIPAddressInformation ipInfo in nif.GetIPProperties().UnicastAddresses) 316 | { 317 | var ip = ipInfo.Address; 318 | if (ip.AddressFamily == AddressFamily.InterNetwork) 319 | { 320 | if(ToBroadcastAddress(ref ip, ipInfo.IPv4Mask)) 321 | ips.Add(ip); 322 | } 323 | } 324 | } 325 | 326 | return ips.ToArray(); 327 | } 328 | 329 | static IPAddress[] GetBroadcastAdressesFromHostEntry() 330 | { 331 | var ips = new List (); 332 | 333 | IPHostEntry hostEntry = Dns.GetHostEntry(Dns.GetHostName()); 334 | 335 | foreach(var address in hostEntry.AddressList) 336 | { 337 | if (address.AddressFamily == AddressFamily.InterNetwork) 338 | { 339 | // this is IPv4 address 340 | // convert it to broadcast address 341 | // use default subnet 342 | 343 | var subnetMask = GetDefaultSubnetMask(address); 344 | if (subnetMask != null) 345 | { 346 | var broadcastAddress = address; 347 | if (ToBroadcastAddress(ref broadcastAddress, subnetMask)) 348 | ips.Add( broadcastAddress ); 349 | } 350 | } 351 | } 352 | 353 | if (ips.Count > 0) 354 | { 355 | // if we found at least 1 ip, then also add full-broadcast address 356 | // this will compensate in case we used a wrong subnet mask 357 | ips.Add(IPAddress.Broadcast); 358 | } 359 | 360 | return ips.ToArray(); 361 | } 362 | 363 | static bool ToBroadcastAddress(ref IPAddress ip, IPAddress subnetMask) 364 | { 365 | if (ip.AddressFamily != AddressFamily.InterNetwork || subnetMask.AddressFamily != AddressFamily.InterNetwork) 366 | return false; 367 | 368 | byte[] bytes = ip.GetAddressBytes(); 369 | byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); 370 | 371 | for(int i=0; i < 4; i++) 372 | { 373 | // on places where subnet mask has 1s, address bits are copied, 374 | // and on places where subnet mask has 0s, address bits are 1 375 | bytes[i] = (byte) ((~subnetMaskBytes[i]) | bytes[i]); 376 | } 377 | 378 | ip = new IPAddress(bytes); 379 | 380 | return true; 381 | } 382 | 383 | static IPAddress GetDefaultSubnetMask(IPAddress ip) 384 | { 385 | if (ip.AddressFamily != AddressFamily.InterNetwork) 386 | return null; 387 | 388 | IPAddress subnetMask; 389 | 390 | byte[] bytes = ip.GetAddressBytes(); 391 | byte firstByte = bytes[0]; 392 | 393 | if (firstByte >= 0 && firstByte <= 127) 394 | subnetMask = new IPAddress(new byte[]{255, 0, 0, 0}); 395 | else if (firstByte >= 128 && firstByte <= 191) 396 | subnetMask = new IPAddress(new byte[]{255, 255, 0, 0}); 397 | else if (firstByte >= 192 && firstByte <= 223) 398 | subnetMask = new IPAddress(new byte[]{255, 255, 255, 0}); 399 | else // undefined subnet 400 | subnetMask = null; 401 | 402 | return subnetMask; 403 | } 404 | 405 | 406 | void OnReceivedServerResponse(DiscoveryInfo info) { 407 | 408 | // check if data is valid 409 | if(!IsDataFromServerValid(info)) 410 | return; 411 | 412 | // invoke event 413 | this.onReceivedServerResponse?.Invoke(info); 414 | 415 | } 416 | 417 | public static bool IsDataFromServerValid(DiscoveryInfo data) 418 | { 419 | // data must contain signature which matches, and port number 420 | return data.KeyValuePairs.ContainsKey(kSignatureKey) && data.KeyValuePairs[kSignatureKey] == GetCachedSignature() 421 | && data.KeyValuePairs.ContainsKey(kPortKey); 422 | } 423 | 424 | 425 | public void RegisterResponseData( string key, string value ) 426 | { 427 | m_responseData[key] = value; 428 | } 429 | 430 | public void UnRegisterResponseData( string key ) 431 | { 432 | m_responseData.Remove (key); 433 | } 434 | 435 | static string GetCachedSignature() 436 | { 437 | if (s_cachedSignature != null) 438 | return s_cachedSignature; 439 | 440 | s_cachedSignature = CalculateGameSignature(); 441 | 442 | return s_cachedSignature; 443 | } 444 | 445 | /// Signature identifies this game among others. 446 | public static string CalculateGameSignature() 447 | { 448 | string[] strings = new string[]{ Application.companyName, Application.productName, 449 | Application.unityVersion }; 450 | 451 | string signature = ""; 452 | 453 | foreach (string str in strings) 454 | { 455 | // only use it's hash code 456 | signature += str.GetHashCode() + "."; 457 | } 458 | 459 | return signature; 460 | } 461 | 462 | 463 | public static string ConvertDictionaryToString( Dictionary dict ) 464 | { 465 | return string.Join( "\n", dict.Select( pair => pair.Key + ": " + pair.Value ) ); 466 | } 467 | 468 | public static Dictionary ConvertStringToDictionary( string str ) 469 | { 470 | var dict = new Dictionary(System.StringComparer.InvariantCulture); 471 | string[] lines = str.Split("\n".ToCharArray(), System.StringSplitOptions.RemoveEmptyEntries); 472 | foreach(string line in lines) 473 | { 474 | int index = line.IndexOf(": "); 475 | if(index < 0) 476 | continue; 477 | dict[line.Substring(0, index)] = line.Substring(index + 2, line.Length - index - 2); 478 | } 479 | return dict; 480 | } 481 | 482 | public static byte[] ConvertDictionaryToByteArray( Dictionary dict ) 483 | { 484 | return ConvertStringToPacketData( ConvertDictionaryToString( dict ) ); 485 | } 486 | 487 | public static Dictionary ConvertByteArrayToDictionary( byte[] data ) 488 | { 489 | return ConvertStringToDictionary( ConvertPacketDataToString( data ) ); 490 | } 491 | 492 | public static byte[] ConvertStringToPacketData(string str) 493 | { 494 | byte[] data = new byte[str.Length * 2]; 495 | for (int i = 0; i < str.Length; i++) 496 | { 497 | ushort c = str[i]; 498 | data[i * 2] = (byte) ((c & 0xff00) >> 8); 499 | data[i * 2 + 1] = (byte) (c & 0x00ff); 500 | } 501 | return data; 502 | } 503 | 504 | public static string ConvertPacketDataToString(byte[] data) 505 | { 506 | char[] arr = new char[data.Length / 2]; 507 | for (int i = 0; i < arr.Length; i++) 508 | { 509 | ushort b1 = data[i * 2]; 510 | ushort b2 = data[i * 2 + 1]; 511 | arr[i] = (char)((b1 << 8) | b2); 512 | } 513 | return new string(arr); 514 | } 515 | 516 | 517 | static bool RunSafe(System.Action action, bool logException = true) 518 | { 519 | try 520 | { 521 | action(); 522 | return true; 523 | } 524 | catch(System.Exception ex) 525 | { 526 | if (logException) 527 | Debug.LogException(ex); 528 | return false; 529 | } 530 | } 531 | 532 | } 533 | 534 | } 535 | -------------------------------------------------------------------------------- /NetworkDiscovery.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65d5e9cc49a255842837a7d853db68fe 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /NetworkDiscoveryHUD.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System.Linq; 5 | using System.Net; 6 | 7 | namespace NetworkDiscoveryUnity 8 | { 9 | 10 | public class NetworkDiscoveryHUD : MonoBehaviour 11 | { 12 | NetworkDiscovery m_networkDiscovery; 13 | 14 | readonly List m_discoveredServers = new List(); 15 | 16 | public List additionalDataToDisplay = new List() 17 | { 18 | NetworkDiscovery.kMapNameKey, 19 | }; 20 | 21 | Vector2 m_scrollViewPos = Vector2.zero; 22 | 23 | public bool IsRefreshing { get { return Time.realtimeSinceStartup - m_timeWhenRefreshed < this.refreshInterval; } } 24 | float m_timeWhenRefreshed = 0f; 25 | 26 | bool m_displayBroadcastAddresses = false; 27 | 28 | IPEndPoint m_lookupServer = null; // server that we are currently looking up 29 | string m_lookupServerIP = ""; 30 | string m_lookupServerPort = NetworkDiscovery.kDefaultServerPort.ToString(); 31 | float m_timeWhenLookedUpServer = 0f; 32 | bool IsLookingUpAnyServer { get { return Time.realtimeSinceStartup - m_timeWhenLookedUpServer < this.refreshInterval 33 | && m_lookupServer != null; } } 34 | 35 | GUIStyle m_centeredLabelStyle; 36 | 37 | public bool drawGUI = true; 38 | public int offsetX = 5; 39 | public int offsetY = 150; 40 | public int width = 500, height = 400; 41 | [Range(1, 5)] public float refreshInterval = 3f; 42 | 43 | public UnityEngine.Events.UnityEvent onConnectEvent 44 | = new UnityEngine.Events.UnityEvent(); 45 | 46 | 47 | void Awake() 48 | { 49 | m_networkDiscovery = this.GetComponent(); 50 | } 51 | 52 | void OnEnable() 53 | { 54 | m_networkDiscovery.onReceivedServerResponse.AddListener(OnDiscoveredServer); 55 | } 56 | 57 | void OnDisable() 58 | { 59 | m_networkDiscovery.onReceivedServerResponse.RemoveListener(OnDiscoveredServer); 60 | } 61 | 62 | void OnGUI() 63 | { 64 | 65 | if (null == m_centeredLabelStyle) 66 | { 67 | m_centeredLabelStyle = new GUIStyle(GUI.skin.label); 68 | m_centeredLabelStyle.alignment = TextAnchor.MiddleCenter; 69 | } 70 | 71 | if (this.drawGUI) 72 | this.Display(new Rect(offsetX, offsetY, width, height)); 73 | 74 | } 75 | 76 | public void Display(Rect displayRect) 77 | { 78 | if (!NetworkDiscovery.SupportedOnThisPlatform) 79 | return; 80 | 81 | GUILayout.BeginArea(displayRect); 82 | 83 | this.DisplayRefreshButton(); 84 | 85 | // lookup a server 86 | 87 | GUILayout.Label("Lookup server: "); 88 | GUILayout.BeginHorizontal(); 89 | GUILayout.Label("IP:"); 90 | m_lookupServerIP = GUILayout.TextField(m_lookupServerIP, GUILayout.Width(120)); 91 | GUILayout.Space(10); 92 | GUILayout.Label("Port:"); 93 | m_lookupServerPort = GUILayout.TextField(m_lookupServerPort, GUILayout.Width(60)); 94 | GUILayout.Space(10); 95 | if (IsLookingUpAnyServer) 96 | { 97 | GUILayout.Button("Lookup...", GUILayout.Height(25), GUILayout.MinWidth(80)); 98 | } 99 | else 100 | { 101 | if (GUILayout.Button("Lookup", GUILayout.Height(25), GUILayout.MinWidth(80))) 102 | LookupServer(); 103 | } 104 | GUILayout.FlexibleSpace(); 105 | GUILayout.EndHorizontal(); 106 | 107 | GUILayout.BeginHorizontal(); 108 | m_displayBroadcastAddresses = GUILayout.Toggle(m_displayBroadcastAddresses, "Display broadcast addresses", GUILayout.ExpandWidth(false)); 109 | if (m_displayBroadcastAddresses) 110 | { 111 | GUILayout.Space(10); 112 | GUILayout.Label( string.Join( ", ", NetworkDiscovery.GetBroadcastAdresses().Select(ip => ip.ToString()) ) ); 113 | } 114 | GUILayout.EndHorizontal(); 115 | 116 | GUILayout.Label(string.Format("Servers [{0}]:", m_discoveredServers.Count)); 117 | 118 | this.DisplayServers(); 119 | 120 | GUILayout.EndArea(); 121 | 122 | } 123 | 124 | public void DisplayRefreshButton() 125 | { 126 | if(IsRefreshing) 127 | { 128 | GUILayout.Button("Refreshing...", GUILayout.Height(25), GUILayout.ExpandWidth(false)); 129 | } 130 | else 131 | { 132 | if (GUILayout.Button("Refresh LAN", GUILayout.Height(25), GUILayout.ExpandWidth(false))) 133 | { 134 | Refresh(); 135 | } 136 | } 137 | } 138 | 139 | public void DisplayServers() 140 | { 141 | 142 | var headerNames = Enumerable.Empty().Append("IP").Concat(this.additionalDataToDisplay); 143 | 144 | int elemWidth = this.width / headerNames.Count() - 5; 145 | 146 | // header 147 | GUILayout.BeginHorizontal(); 148 | foreach(string str in headerNames) 149 | GUILayout.Button(str, GUILayout.Width(elemWidth)); 150 | GUILayout.EndHorizontal(); 151 | 152 | // servers 153 | 154 | m_scrollViewPos = GUILayout.BeginScrollView(m_scrollViewPos); 155 | 156 | foreach(var info in m_discoveredServers) 157 | { 158 | GUILayout.BeginHorizontal(); 159 | 160 | bool hasGameServerPort = info.TryGetGameServerPort(out ushort gameServerPort); 161 | 162 | if( GUILayout.Button(info.EndPoint.Address.ToString() + (hasGameServerPort ? $":{gameServerPort}" : ""), GUILayout.Width(elemWidth)) ) 163 | this.onConnectEvent.Invoke(info); 164 | 165 | foreach(string headerName in headerNames.Skip(1)) 166 | { 167 | GUILayout.Label( 168 | info.KeyValuePairs.TryGetValue(headerName, out string value) ? value : "", 169 | m_centeredLabelStyle, 170 | GUILayout.Width(elemWidth)); 171 | } 172 | 173 | GUILayout.EndHorizontal(); 174 | } 175 | 176 | GUILayout.EndScrollView(); 177 | 178 | } 179 | 180 | public void Refresh() 181 | { 182 | m_discoveredServers.Clear(); 183 | 184 | m_timeWhenRefreshed = Time.realtimeSinceStartup; 185 | 186 | m_networkDiscovery.SendBroadcast(); 187 | 188 | } 189 | 190 | public void LookupServer() 191 | { 192 | // parse IP and port 193 | 194 | IPAddress ip = IPAddress.Parse(m_lookupServerIP); 195 | ushort port = ushort.Parse(m_lookupServerPort); 196 | 197 | // input is ok 198 | // send discovery request 199 | 200 | m_timeWhenLookedUpServer = Time.realtimeSinceStartup; 201 | 202 | m_lookupServer = new IPEndPoint(ip, port); 203 | 204 | m_networkDiscovery.SendDiscoveryRequest(m_lookupServer); 205 | } 206 | 207 | bool IsLookingUpServer(IPEndPoint endPoint) 208 | { 209 | return Time.realtimeSinceStartup - m_timeWhenLookedUpServer < this.refreshInterval 210 | && m_lookupServer != null 211 | && m_lookupServer.Equals(endPoint); 212 | } 213 | 214 | void OnDiscoveredServer(NetworkDiscovery.DiscoveryInfo info) 215 | { 216 | if (!IsRefreshing && !IsLookingUpServer(info.EndPoint)) 217 | return; 218 | 219 | int index = m_discoveredServers.FindIndex(item => item.EndPoint.Equals(info.EndPoint)); 220 | if(index < 0) 221 | { 222 | // server is not in the list 223 | // add it 224 | m_discoveredServers.Add(info); 225 | } 226 | else 227 | { 228 | // server is in the list 229 | // update it 230 | m_discoveredServers[index] = info; 231 | } 232 | 233 | } 234 | 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /NetworkDiscoveryHUD.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 97d06c4b188731a47baf1a65a4b8a512 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /NetworkDiscoveryUnity.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NetworkDiscoveryUnity" 3 | } 4 | -------------------------------------------------------------------------------- /NetworkDiscoveryUnity.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2218330688c91a143b7c0a45114ecf66 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## NetworkDiscoveryUnity 3 | 4 | Network discovery for Unity3D. 5 | 6 | 7 | ## Features 8 | 9 | - Simple. 1 script. 600 lines of code. 10 | 11 | - Uses C#'s UDP sockets for broadcasting and sending responses. 12 | 13 | - Independent of current networking framework. 14 | 15 | - Single-threaded. 16 | 17 | - Tested on: Linux, Windows, Android. 18 | 19 | - Can lookup specific servers on the internet (outside of local network). 20 | 21 | - Has a separate GUI script (`NetworkDiscoveryHUD`) for easy testing. 22 | 23 | - Has support for custom response data. 24 | 25 | - By default, server responds with: current scene, game server port number, game signature. 26 | 27 | - No impact on performance. 28 | 29 | 30 | ## Usage 31 | 32 | Attach `NetworkDiscovery` script to any game object. Assign game server port number. 33 | 34 | Now, you can use `NetworkDiscoveryHUD` script to test it (by attaching it to the same game object), or use the API directly: 35 | 36 | ```cs 37 | // when your game server starts, initialize the network discovery 38 | networkDiscovery.EnsureServerIsInitialized(); 39 | 40 | // register listener - this can also be done from Inspector 41 | networkDiscovery.onReceivedServerResponse.AddListener((NetworkDiscovery.DiscoveryInfo info) => 42 | { 43 | // we received response from server 44 | // add it to list of servers, or connect immediately... 45 | }); 46 | 47 | // send broadcast on LAN - this is used when you want to refresh the list of servers 48 | // when server receives the packet, he will respond 49 | networkDiscovery.SendBroadcast(); 50 | 51 | // on server side, you can register custom data for responding 52 | networkDiscovery.RegisterResponseData("Game mode", "Deathmatch"); 53 | 54 | // when your game server is stopped, shutdown the network discovery 55 | networkDiscovery.CloseServerUdpClient(); 56 | ``` 57 | 58 | For more details on how to use it, check out `NetworkDiscoveryHUD` script. 59 | 60 | 61 | ## Inspector 62 | 63 | ![](https://i.imgur.com/VuLPOzQ.png) 64 | 65 | 66 | ## Example GUI 67 | 68 | ![](https://i.imgur.com/SXqKMbJ.png) 69 | 70 | 71 | ## Further improvements 72 | 73 | - Measure ping - requires that all socket operations are done in a separate thread, or using async methods 74 | 75 | - Prevent detection of multiple localhost servers (by assigning GUID to each packet) ? 76 | 77 | - Add "Refresh" button in GUI next to each server 78 | 79 | - Catch the other exception which is thrown on windows - it's harmless, so it should not be logged 80 | 81 | - Make sure packet-to-string conversion works with non-ascii characters 82 | 83 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cc18b7fbfe180214c8532693717231b8 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------