From 22640b5e6253a186f4fe7ab26099ca00e49a5f11 Mon Sep 17 00:00:00 2001 From: vis2k Date: Wed, 20 Mar 2019 09:38:53 +0100 Subject: [PATCH] Mirror List Server example (#611) feature: Mirror List Server example --- Assets/Mirror/Examples/ListServer.meta | 8 + .../Mirror/Examples/ListServer/ListServer.cs | 307 ++ .../Examples/ListServer/ListServer.cs.meta | 11 + Assets/Mirror/Examples/ListServer/Scenes.meta | 8 + .../Examples/ListServer/Scenes/NavMesh.asset | Bin 0 -> 5444 bytes .../ListServer/Scenes/NavMesh.asset.meta | 8 + .../Examples/ListServer/Scenes/Scene.unity | 3387 +++++++++++++++++ .../ListServer/Scenes/Scene.unity.meta | 8 + Assets/Mirror/Examples/ListServer/UI.meta | 8 + .../ListServer/UI/ServerStatusSlot.prefab | 651 ++++ .../UI/ServerStatusSlot.prefab.meta | 7 + .../ListServer/UI/UIServerStatusSlot.cs | 13 + .../ListServer/UI/UIServerStatusSlot.cs.meta | 11 + ProjectSettings/EditorBuildSettings.asset | 1 + 14 files changed, 4428 insertions(+) create mode 100644 Assets/Mirror/Examples/ListServer.meta create mode 100644 Assets/Mirror/Examples/ListServer/ListServer.cs create mode 100644 Assets/Mirror/Examples/ListServer/ListServer.cs.meta create mode 100644 Assets/Mirror/Examples/ListServer/Scenes.meta create mode 100644 Assets/Mirror/Examples/ListServer/Scenes/NavMesh.asset create mode 100644 Assets/Mirror/Examples/ListServer/Scenes/NavMesh.asset.meta create mode 100644 Assets/Mirror/Examples/ListServer/Scenes/Scene.unity create mode 100644 Assets/Mirror/Examples/ListServer/Scenes/Scene.unity.meta create mode 100644 Assets/Mirror/Examples/ListServer/UI.meta create mode 100644 Assets/Mirror/Examples/ListServer/UI/ServerStatusSlot.prefab create mode 100644 Assets/Mirror/Examples/ListServer/UI/ServerStatusSlot.prefab.meta create mode 100644 Assets/Mirror/Examples/ListServer/UI/UIServerStatusSlot.cs create mode 100644 Assets/Mirror/Examples/ListServer/UI/UIServerStatusSlot.cs.meta diff --git a/Assets/Mirror/Examples/ListServer.meta b/Assets/Mirror/Examples/ListServer.meta new file mode 100644 index 000000000..af33e59ef --- /dev/null +++ b/Assets/Mirror/Examples/ListServer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e192f90e0acbb41f88dfe3dba300a5c9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/ListServer/ListServer.cs b/Assets/Mirror/Examples/ListServer/ListServer.cs new file mode 100644 index 000000000..86fadd9ff --- /dev/null +++ b/Assets/Mirror/Examples/ListServer/ListServer.cs @@ -0,0 +1,307 @@ +// add this component to the NetworkManager +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; + +namespace Mirror.Examples.Listen +{ + public class ServerStatus + { + public string ip; + //public ushort port; // <- not all transports use a port. assume default port. feel free to also send a port if needed. + public string title; + public ushort players; + public ushort capacity; + + public int lastLatency = -1; + public Ping ping; + + public ServerStatus(string ip, /*ushort port,*/ string title, ushort players, ushort capacity) + { + this.ip = ip; + //this.port = port; + this.title = title; + this.players = players; + this.capacity = capacity; + ping = new Ping(ip); + } + } + + [RequireComponent(typeof(NetworkManager))] + public class ListServer : MonoBehaviour + { + [Header("Listen Server Connection")] + public string listServerIp = "127.0.0.1"; + public ushort gameServerToListenPort = 8887; + public ushort clientToListenPort = 8888; + public string gameServerTitle = "Deathmatch"; + + Telepathy.Client gameServerToListenConnection = new Telepathy.Client(); + Telepathy.Client clientToListenConnection = new Telepathy.Client(); + + [Header("UI")] + public GameObject mainPanel; + public Transform content; + public UIServerStatusSlot slotPrefab; + public Button serverAndPlayButton; + public Button serverOnlyButton; + public GameObject connectingPanel; + public Text connectingText; + public Button connectingCancelButton; + int connectingDots = 0; + + // all the servers, stored as dict with unique ip key so we can + // update them more easily + // (use "ip:port" if port is needed) + Dictionary list = new Dictionary(); + + void Start() + { + // examples + //list["127.0.0.1"] = new ServerStatus("127.0.0.1", "Deathmatch", 3, 10); + //list["192.168.0.1"] = new ServerStatus("192.168.0.1", "Free for all", 7, 10); + //list["172.217.22.3"] = new ServerStatus("172.217.22.3", "5vs5", 10, 10); + //list["172.217.16.142"] = new ServerStatus("172.217.16.142", "Hide & Seek Mod", 0, 10); + + // Update once a second. no need to try to reconnect or read data + // in each Update call + // -> calling it more than 1/second would also cause significantly + // more broadcasts in the list server. + InvokeRepeating(nameof(Tick), 0, 1); + } + + bool IsConnecting() => NetworkClient.active && !ClientScene.ready; + bool FullyConnected() => NetworkClient.active && ClientScene.ready; + + // should we use the client to listen connection? + bool UseClientToListen() + { + return !NetworkManager.IsHeadless() && !NetworkServer.active && !FullyConnected(); + } + + // should we use the game server to listen connection? + bool UseGameServerToListen() + { + return NetworkServer.active; + } + + void Tick() + { + TickGameServer(); + TickClient(); + } + + // send server status to list server + void SendStatus() + { + BinaryWriter writer = new BinaryWriter(new MemoryStream()); + + // create message + // NOTE: you can send anything that you want, as long as you also + // receive it in ParseMessage + char[] titleChars = gameServerTitle.ToCharArray(); + writer.Write((ushort)titleChars.Length); + writer.Write(titleChars); + writer.Write((ushort)NetworkServer.connections.Count); + writer.Write((ushort)NetworkManager.singleton.maxConnections); + + // send it + writer.Flush(); + gameServerToListenConnection.Send(((MemoryStream)writer.BaseStream).ToArray()); + } + + void TickGameServer() + { + // send server data to listen + if (UseGameServerToListen()) + { + // connected yet? + if (gameServerToListenConnection.Connected) + { + SendStatus(); + } + // otherwise try to connect + // (we may have just started the game) + else if (!gameServerToListenConnection.Connecting) + { + Debug.Log("Establishing game server to listen connection..."); + gameServerToListenConnection.Connect(listServerIp, gameServerToListenPort); + } + } + // shouldn't use game server, but still using it? + else if (gameServerToListenConnection.Connected) + { + gameServerToListenConnection.Disconnect(); + } + } + + void ParseMessage(byte[] bytes) + { + // use binary reader because our NetworkReader uses custom string reading with bools + // => we don't use ReadString here because the listen server doesn't + // know C#'s '7-bit-length + utf8' encoding for strings + BinaryReader reader = new BinaryReader(new MemoryStream(bytes, false), Encoding.UTF8); + ushort ipLength = reader.ReadUInt16(); + string ip = new string(reader.ReadChars(ipLength)); + //ushort port = reader.ReadUInt16(); <- not all Transports use a port. assume default. + ushort titleLength = reader.ReadUInt16(); + string title = new string(reader.ReadChars(titleLength)); + ushort players = reader.ReadUInt16(); + ushort capacity = reader.ReadUInt16(); + //Debug.Log("PARSED: ip=" + ip + /*" port=" + port +*/ " title=" + title + " players=" + players + " capacity= " + capacity); + + // build key + string key = ip/* + ":" + port*/; + + // find existing or create new one + ServerStatus server; + if (list.TryGetValue(key, out server)) + { + // refresh + server.title = title; + server.players = players; + server.capacity = capacity; + } + else + { + // create + server = new ServerStatus(ip, /*port,*/ title, players, capacity); + } + + // save + list[key] = server; + } + + void TickClient() + { + // receive client data from listen + if (UseClientToListen()) + { + // connected yet? + if (clientToListenConnection.Connected) + { + // receive latest game server info + while (clientToListenConnection.GetNextMessage(out Telepathy.Message message)) + { + // data message? + if (message.eventType == Telepathy.EventType.Data) + ParseMessage(message.data); + } + + // ping again if previous ping finished + foreach (ServerStatus server in list.Values) + { + if (server.ping.isDone) + { + server.lastLatency = server.ping.time; + server.ping = new Ping(server.ip); + } + } + } + // otherwise try to connect + // (we may have just joined the menu/disconnect from game server) + else if (!clientToListenConnection.Connecting) + { + Debug.Log("Establishing client to listen connection..."); + clientToListenConnection.Connect(listServerIp, clientToListenPort); + } + } + // shouldn't use client, but still using it? (e.g. after joining) + else if (clientToListenConnection.Connected) + { + clientToListenConnection.Disconnect(); + list.Clear(); + } + + // refresh UI afterwards + OnUI(); + } + + // instantiate/remove enough prefabs to match amount + public static void BalancePrefabs(GameObject prefab, int amount, Transform parent) + { + // instantiate until amount + for (int i = parent.childCount; i < amount; ++i) + { + GameObject go = GameObject.Instantiate(prefab); + go.transform.SetParent(parent, false); + } + + // delete everything that's too much + // (backwards loop because Destroy changes childCount) + for (int i = parent.childCount-1; i >= amount; --i) + GameObject.Destroy(parent.GetChild(i).gameObject); + } + + void OnUI() + { + // only show while client not connected and server not started + if (!NetworkManager.singleton.isNetworkActive || IsConnecting()) + { + mainPanel.SetActive(true); + + // instantiate/destroy enough slots + BalancePrefabs(slotPrefab.gameObject, list.Count, content); + + // refresh all members + for (int i = 0; i < list.Values.Count; ++i) + { + UIServerStatusSlot slot = content.GetChild(i).GetComponent(); + ServerStatus server = list.Values.ToList()[i]; + slot.titleText.text = server.title; + slot.playersText.text = server.players + "/" + server.capacity; + slot.latencyText.text = server.lastLatency != -1 ? server.lastLatency.ToString() : "..."; + slot.addressText.text = server.ip; + slot.joinButton.interactable = !IsConnecting(); + slot.joinButton.gameObject.SetActive(server.players < server.capacity); + slot.joinButton.onClick.RemoveAllListeners(); + slot.joinButton.onClick.AddListener(() => { + NetworkManager.singleton.networkAddress = server.ip; + NetworkManager.singleton.StartClient(); + }); + } + + // server buttons + serverAndPlayButton.interactable = !IsConnecting(); + serverAndPlayButton.onClick.RemoveAllListeners(); + serverAndPlayButton.onClick.AddListener(() => { + NetworkManager.singleton.StartHost(); + }); + + serverOnlyButton.interactable = !IsConnecting(); + serverOnlyButton.onClick.RemoveAllListeners(); + serverOnlyButton.onClick.AddListener(() => { + NetworkManager.singleton.StartServer(); + }); + } + else mainPanel.SetActive(false); + + // show connecting panel while connecting + if (IsConnecting()) + { + connectingPanel.SetActive(true); + + // . => .. => ... => .... + connectingDots = ((connectingDots + 1) % 4); + connectingText.text = "Connecting" + new string('.', connectingDots); + + // cancel button + connectingCancelButton.onClick.RemoveAllListeners(); + connectingCancelButton.onClick.AddListener(NetworkManager.singleton.StopClient); + } + else connectingPanel.SetActive(false); + } + + // disconnect everything when pressing Stop in the Editor + void OnApplicationQuit() + { + if (gameServerToListenConnection.Connected) + gameServerToListenConnection.Disconnect(); + if (clientToListenConnection.Connected) + clientToListenConnection.Disconnect(); + } + } +} diff --git a/Assets/Mirror/Examples/ListServer/ListServer.cs.meta b/Assets/Mirror/Examples/ListServer/ListServer.cs.meta new file mode 100644 index 000000000..35af6c4d3 --- /dev/null +++ b/Assets/Mirror/Examples/ListServer/ListServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69f796b44735c414783d66f47b150c5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/ListServer/Scenes.meta b/Assets/Mirror/Examples/ListServer/Scenes.meta new file mode 100644 index 000000000..e0412be38 --- /dev/null +++ b/Assets/Mirror/Examples/ListServer/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9826d673f42ad4c59b8fc27ae86729e1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/ListServer/Scenes/NavMesh.asset b/Assets/Mirror/Examples/ListServer/Scenes/NavMesh.asset new file mode 100644 index 0000000000000000000000000000000000000000..3acffc80e7884117f6ba1c24763179df0d46214e GIT binary patch literal 5444 zcmb`KTWl0%6vxlD+ZGYAMNn>HQ9(ed-Ihy{i`%lT1-X>ARODiv?oQj0z0}=VXu-<} z0t$u)W8#xZNrag8!NezhFex#<5b#BOFi{^&Fg_U`j1N9o&;Pr1=c7tv9GIQ+{pQU7 zJLi0tnG#jsAX<mN3i3(^eM}e6 zJ;pyA&uqgZ)Xs5^b8x7`z2Lzt#1n~eUO2&#b0iL4F7jQ%&miCXgpTC<*!LAa5yxD^ zBlI2n?%+0b?&oVwfLO!&)!-B0VSOHBjI-be_zHdK%mWX=qe6d>+u#ovUJ;6aK6o7d zm7(|-fb$7H%5m!WA2d8l1-4J}i5MR;yh8Y&;x_VKXz>e*FS7UtiZ8bKM~W}8_!Y&M zTKrSRAGY}CiZ8SHRmC5%_%+3AE&i?I%Pszc;+XHUZn6%)DE^qmu|83s$1VP+;wvm3 zWoP)WwD?@bpHRGvy%sWC%jbG#tN5%A@T^=%3FeretHJpMzr=H=L-92hKdd;`YpMUB z;!jz;p!iygzoB@Y#V;!Uw8cMGyuO5EKjHmH#2+i+mzZBwyrG1{|C-{B7XMyx>^V9w z`2VapYNz?{%;xgO#uwTE9>6Xa^L)PIykAmv99*vJrOdJJnv8#VU2ih};dT8CIG@m8 z%^FyDSVNl)kJ2q(j~n?O_MR<**Rp?w7{@qsjPF*9wj=Z`AM_!eoyt*x}^V)B5omY?H zQvXAx|AOIR{R0-)`Y&2s>mM{+)@@YP?GSiyXWb5i$EWI}-@iw|`9!{-um%LtBo zhWDkc#dW?pi|c%c443-1mHw-ShxPLo*ZTNYsivuTw7ze6RE+x{tdDvY1xJ0NbbiKQ*X#6zpop_PG!OBeMIwI5 zbzZMoT<7(=;Zk3((>Dwc>%VDntshui>%V1qgl_TsqMq}@JUeB${I1aNqth1G`fpoY z>z}dsX4c2JoHd;L<+^QUj`?=Z@NmEHSX}q}uElk~=M9gD^~HY!h<@?iiery+Y`0q& zOgO$n9UXptOKZyWbNSX`-^~`hTy`5}dfU9TyCcCyhvN^jA^bY>ZokvFBU|vDY|@3B zlImvfg4RVtXlHs&H|CMSNA>ql|QpU@6x&u5I&3V_M zjFT-o>FsVh-Q}HdDX9&~QE&Y@hv^Eo0K=9Pbfd^ewWysR2&>5B2@i7?ercYEBt?XKrj&qxj ziTL=~`HAtdF??%_!?xvp=>uDC_q?Oo$6ZSDE@#vW#D%h%HLX_;Y62 zl-s{prFp{b?s*C^k!R2T%kk1YaYyIId2rq`XKl;eq|Y>Y^3&0Pe`b^?{Gzn@Kjo=& zEbZ%`#}i&8#uNEsJkdsO$V=PuIyKJ7Z^{R+Z#kY6R0i etc. +using UnityEngine; +using UnityEngine.UI; + +public class UIServerStatusSlot : MonoBehaviour +{ + public Text titleText; + public Text playersText; + public Text latencyText; + public Text addressText; + public Button joinButton; +} diff --git a/Assets/Mirror/Examples/ListServer/UI/UIServerStatusSlot.cs.meta b/Assets/Mirror/Examples/ListServer/UI/UIServerStatusSlot.cs.meta new file mode 100644 index 000000000..a3fee6dc4 --- /dev/null +++ b/Assets/Mirror/Examples/ListServer/UI/UIServerStatusSlot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f24d39c9182b4098bcfd0c2546d0bf2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset index 6dc24f7df..0147887ef 100644 --- a/ProjectSettings/EditorBuildSettings.asset +++ b/ProjectSettings/EditorBuildSettings.asset @@ -5,3 +5,4 @@ EditorBuildSettings: m_ObjectHideFlags: 0 serializedVersion: 2 m_Scenes: [] + m_configObjects: {}