此部分可以先建立游戏大厅,然后进入游戏,此处坑甚多耗费大量时间。国内百度出来的基本没靠谱的,一些专栏作家大V也不过是基本翻译了一下用户手册(坑啊),只能通过看youtube视频以及不停的翻阅用户手册解决(很多demo根本没踩到这些坑)。废话不说了,开始(此部分是在上一篇基础上开始的,一些上一篇解释过的操作就不再重复),建议看完此部分后可以看一下下一篇,有助于理解此部分的代码:
1)自定义NetworkLobbyManager
自定义脚本LobbyManager并重写NetworkLobbyManager的虚方法,惯例脚本在文末。
2)挂载脚本LobbyManager
挂载脚本是有讲究的,此脚本继承自NetworkLobbyManager。NetworkLobbyManager需要两个场景,即lobby场景和游戏场景,前者obby场景为准备场景(可以是像王者荣耀开始前队友列表UI,可以像吃鸡游戏开始之前的小岛场景),后者game场景即为真实游戏进行的场景。同时,对应的 需要两个参数lobby Player和gamePlayer,lobbyPlayer为进入lobby的游戏物体,game Player为真是游戏时的游戏物体。NetworkLobbyManager根据lobby场景中的lobbyPlayer在游戏场景中生成gamePlayer,但是当加载游戏场景后如果lobbyPlayer被销毁,则无法生成gamePlayer,所以加载场景时不能销毁。 而NetworkLobbyManager继承自NetworkManager,所以有DontDestroyOnLoad选项(自定义脚本不需要再继续添加DontDestroyOnLoad选项。而且在某些非常态lobby场景重新加载时(稍后会说到一些可能的重新加载场景)会有多个NetworkLobbyManager),所以生成的lobby Player最好是NetworkLobbyManager的子游戏物体。
3)游戏列表
游戏列表其实为了把unityService中建立的游戏进行列表,可供玩家选择加入。不同的游戏是通过LobbyMatch脚本控制,在有些列表中把每一个LobbyMatch实例化出来
4)Player定义
gamePlayer与上一篇(其实是本系列第一篇)中NetworkManager的game Player定义相同,必须包含NetworkIdentity和NetworkTransform(如果需要),在增加其他诸如移动,血量控制的脚本。lobbyPlayer则必须继承自Network LobbyPlayer。本篇通过自定义的LobbyPlayer(继承自Network LobbyPlayer)以及Lobby PlayerUI(lobby场景中LobbyPlayer为ui形态),两者脚本在文末。
PS:通过对NetworkLobbyManager应用发现很多坑,所幸基本坑都填上,坑实在太多,但是大部分均在代码中注释清楚,更加详细解释单独在下篇文章中解释。
//———————————————————————脚本————————————————————————//
using UnityEngine.Networking; using UnityEngine.Networking.Match; using System; using System.Collections.Generic; using UnityEngine; public class LobbyManager : NetworkLobbyManager { public static LobbyManager theLobbyManager; #region OUTER ACITONS public Action<MatchInfo> matchCreatedAction; public Action<List<MatchInfoSnapshot>> matchListedAction; public Action destroyMatchAction; public Action dropConnAction; public Action clientSceneChangedAction; #endregion #region HOST CALLBACKS public override void OnStartHost() { LogInfo.theLogger.Log("Host create"); base.OnStartHost(); } public override void OnStopHost() { LogInfo.theLogger.Log("Host stop"); base.OnStopHost(); } public override void OnStopClient() { LogInfo.theLogger.Log("Host client"); base.OnStopClient(); } public override void OnStartClient(NetworkClient lobbyClient) { LogInfo.theLogger.Log("Client create"); base.OnStartClient(lobbyClient); } public override GameObject OnLobbyServerCreateLobbyPlayer(NetworkConnection conn, short playerControllerId) { int num = 0; foreach (var p in lobbySlots) { if (p != null) { num++; } } LogInfo.theLogger.Log("ServerCreateLobbyPlayer:" + num); return base.OnLobbyServerCreateLobbyPlayer(conn, playerControllerId); ; } public override GameObject OnLobbyServerCreateGamePlayer(NetworkConnection conn, short playerControllerId) { LogInfo.theLogger.Log("ServerCreateGamePlayer:" + lobbySlots.Length); return base.OnLobbyServerCreateGamePlayer(conn, playerControllerId); } public override void OnLobbyServerPlayerRemoved(NetworkConnection conn, short playerControllerId) { LogInfo.theLogger.Log("LobbyServerPlayerRemoved"); base.OnLobbyServerPlayerRemoved(conn, playerControllerId); } public override void OnServerRemovePlayer(NetworkConnection conn, PlayerController player) { LogInfo.theLogger.Log("ServerRemovePlayer"); base.OnServerRemovePlayer(conn, player); } public override void OnLobbyClientSceneChanged(NetworkConnection conn) { LogInfo.theLogger.Log("LobbyClientSceneChanged"); base.OnLobbyClientSceneChanged(conn); } public override void OnLobbyServerPlayersReady()//当所有当前加入的player均准备后调用此方法,但加入的玩家数不一定定于maxPlayer { LogInfo.theLogger.Log("LobbyServerPlayersReady:"+lobbySlots.Length); //base方法当所有加入的player均准备后进入游戏场景 //base.OnLobbyServerPlayersReady(); int readyPlayersNum = 0; foreach(var player in lobbySlots) { if (player != null && player.readyToBegin) readyPlayersNum++; } if (readyPlayersNum == maxPlayers) BeginGame(); } #endregion #region MATCH CALLBACKS public override void OnMatchCreate(bool success, string extendedInfo, MatchInfo matchInfo) { LogInfo.theLogger.Log("Match create"); base.OnMatchCreate(success, extendedInfo, matchInfo); if (matchCreatedAction != null) { matchCreatedAction(matchInfo); } } public override void OnMatchJoined(bool success, string extendedInfo, MatchInfo matchInfo) { LogInfo.theLogger.Log("Match joined_" + matchInfo.address); Debug.Log("Before callback Manager==null:" + (theLobbyManager == null)); base.OnMatchJoined(success, extendedInfo, matchInfo); Debug.Log("After callback Manager==null:" + (theLobbyManager == null)); } public override void OnMatchList(bool success, string extendedInfo, List<MatchInfoSnapshot> matchList) { base.OnMatchList(success, extendedInfo, matchList); LogInfo.theLogger.Log("Match list:" + matchList.Count); if (matchListedAction != null) { matchListedAction(matchList); } } public override void OnDestroyMatch(bool success, string extendedInfo) { LogInfo.theLogger.Log("DestroyMatch"); base.OnDestroyMatch(success, extendedInfo); StopMatchMaker(); StopHost();//Local Connection already exists if (destroyMatchAction != null) { destroyMatchAction(); } } public override void OnDropConnection(bool success, string extendedInfo) { LogInfo.theLogger.Log("DropConnection"); base.OnDropConnection(success, extendedInfo); if (dropConnAction != null) { dropConnAction(); } } #endregion #region CLIENT CALLBACKS public override void OnClientConnect(NetworkConnection conn) { LogInfo.theLogger.Log("Client connect"); base.OnClientConnect(conn); LogInfo.theLogger.Log("ClientScene players:"+ClientScene.localPlayers.Count); conn.RegisterHandler(LobbyPlayer.playerMsg, HandlePlayMsg); } public override void OnClientDisconnect(NetworkConnection conn) { LogInfo.theLogger.Log("Client disconnect"); base.OnClientDisconnect(conn); } public override void OnClientSceneChanged(NetworkConnection conn) { LogInfo.theLogger.Log("ClientSceneChanged"); Debug.Log("ClientSceneChanged"); if(clientSceneChangedAction!=null) { clientSceneChangedAction(); } //base中回调执行ClientScene.AddPlayer(conn, 0, msg); //A connection has already been set as ready.There can only be one. //UnityEngine.Networking.NetworkLobbyManager:OnClientSceneChanged(NetworkConnection) //base.OnClientSceneChanged(conn); //if (string.IsNullOrEmpty(this.onlineScene) || this.onlineScene == this.offlineScene) //{ // ClientScene.Ready(conn); // if (this.autoCreatePlayer) // { // ClientScene.AddPlayer(conn, 0, new PlayerMsg()); // } //} } //class PlayerMsg : MessageBase { } #endregion private void HandlePlayMsg(NetworkMessage netMsg) { netMsg.conn.Disconnect(); } private void BeginGame() { LogInfo.theLogger.Log("开始"); ServerChangeScene(playScene); } //private void Awake() //{ //DontDestroyOnLoad(gameObject); //theLobbyManager = this; //} //private void Update() //{ //Debug.Log("this==null:" + (this == null)); //if(theLobbyManager==null) //{ // theLobbyManager = this; // Debug.Log("theManager Reset"); //} //} }
using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking.Match; using UnityEngine.UI; public class LobbyMainMenu : MonoBehaviour { public LobbyManager lobbyManager; public Button createMatchBtn; public Button playersBackToMain; public Button serversBackToMain; public Button listServerBtn; public InputField matchName; public GameObject gameListPanel; public GameObject serverListPanel; public GameObject lobbyMatch; public static LobbyMainMenu mainMenu; public void AddPlayer(LobbyPlayer player) { gameListPanel.SetActive(true); serverListPanel.SetActive(false); player.transform.SetParent(gameListPanel.transform.Find("PlayerListPanel")); } public void DropConnection() { //lobbyManager = LobbyManager.theLobbyManager; Debug.Log("Before drop_MM_" + (lobbyManager == null) + "_MK_" + (lobbyManager.matchMaker == null)); if (lobbyManager.matchMaker == null) lobbyManager.StartMatchMaker();//此语句为了测试增加,此处可以去掉 lobbyManager.matchMaker.DropConnection(lobbyManager.matchInfo.networkId, lobbyManager.matchInfo.nodeId, 0, lobbyManager.OnDropConnection); Debug.Log("After drop_MM_" + (lobbyManager == null) + "_MK_" + (lobbyManager.matchMaker == null)); } public void DestroyMatch() { //lobbyManager.matchMaker.DropConnection(lobbyManager.matchInfo.networkId, // lobbyManager.matchInfo.nodeId, 0, lobbyManager.OnDropConnection); lobbyManager.matchMaker.DestroyMatch(lobbyManager.matchInfo.networkId, 0, lobbyManager.OnDestroyMatch); } private void CreateMatch() { if (lobbyManager.matchMaker == null) lobbyManager.StartMatchMaker(); lobbyManager.matchMaker.CreateMatch(matchName.text, 5, true, "", "", "", 0, 0, lobbyManager.OnMatchCreate); } private void ListServers() { if (lobbyManager.matchMaker == null) lobbyManager.StartMatchMaker(); lobbyManager.matchMaker.ListMatches(0, 5, "", true, 0, 0, lobbyManager.OnMatchList); } private void PlayerListBackToMainMenu() { //lobbyManager.matchMaker.DestroyMatch(lobbyManager.matchInfo.networkId, 0, //lobbyManager.OnDestroyMatch); //lobbyManager.matchMaker.DropConnection(lobbyManager.matchInfo.networkId, //lobbyManager.matchInfo.nodeId, 0, lobbyManager.OnDestroyMatch); gameListPanel.SetActive(false); gameObject.SetActive(true); } private void ServerListBackToMainMenu() { serverListPanel.SetActive(false); gameObject.SetActive(true); } private void OnMatchCreate(MatchInfo obj) { gameListPanel.SetActive(true); gameObject.SetActive(false); } private void OnMatchList(List<MatchInfoSnapshot> matchList) { foreach(Transform tt in serverListPanel.transform.Find("ServerListPanel")) { Destroy(tt.gameObject); } foreach(var match in matchList) { GameObject matchGo = Instantiate(lobbyMatch, serverListPanel.transform.Find("ServerListPanel")); matchGo.GetComponent<LobbyMatch>().Populate(match, lobbyManager); } serverListPanel.SetActive(true); gameObject.SetActive(false); } private void OnDestroyMatch() { gameListPanel.SetActive(false); gameObject.SetActive(true); } private void OnClientSceneChanged() { //加载游戏场景后,将主场景ui关掉 //后续此部分还要处理,返回时还需重新打开ui gameObject.SetActive(false); gameListPanel.SetActive(false); } private void Start() { mainMenu = this; //lobbyManager = LobbyManager.theLobbyManager; createMatchBtn.onClick.AddListener(CreateMatch); listServerBtn.onClick.AddListener(ListServers); playersBackToMain.onClick.AddListener(PlayerListBackToMainMenu); serversBackToMain.onClick.AddListener(ServerListBackToMainMenu); lobbyManager.matchCreatedAction += OnMatchCreate; lobbyManager.matchListedAction += OnMatchList; lobbyManager.destroyMatchAction += OnDestroyMatch; } }
using System; using UnityEngine; using UnityEngine.Networking; /// <summary> /// isLocalPayer,isServer,isClient区别: /// OnClientEnterLobby回调时只是数据层面已经客户端连接进来,但是还没有建立client的gameobject,所以此时isLocalPlayer并未赋值(默认false) /// 三者的区别从定义以及测试得知如下: /// isServer:在服务端的活动的player,此值均为true,即不管是server端StartHost时自己建立的client还是Server Spawn产生,只要在服务端显示,此值均为true /// isClient:由Server端spawn产生,并作为client运行的,此值均为true,即只要是Spawn产生的,不管是在服务端还是在客户端,此值均为true /// 所以,服务端存在的player,isServer和isClient均为true,客户端isclient均为true,isServer均为false。(isServer与isClient并没有对立关系) /// isLocalPayer在执行OnStartLocalPlayer回调时赋值为true,即生成本地的player时赋值,且当Server端spawn的client加入到场景中时只执行OnClientEnterLobby回调, /// 不执行OnStartLocalPlayer回调,它的作用是识别玩家控制的游戏物体 /// </summary> public class LobbyPlayer : NetworkLobbyPlayer { public Action<bool> clientEnterAction; public Action<bool,bool> localPlayerAction; public Action<bool> clientReadyAction; public static short playerMsg = MsgType.Highest + 11; #region METHODS public void RemovePlayer_() { if (isLocalPlayer) { //如果调用RemovePlayer以及ClientScene的RemovePlayer时只会删除服务端与客户端的player //而客户端与服务端的NetworkConnection不会销毁(RemovePlayer亲测,ClientScene的RemovePlayer未测试,只是看释义) //所以当离开游戏继续加入游戏时会报Error:A connection has already set as ready.... //因为每次重新连接时,connection是肯定存在,没有被销毁,所以不确定后续的风险 //所以此处调用matchMaker.DropConnection //但是用DropConnection方法会引发Error:ClientDisconnected due to error: Timeout(不确定是否为本身bug) //但是此问题与Error:ServerDisconnected due to error: Timeout(本身bug,有说此bug已修复,但是作者2017版本依然存在,但为下载补丁(下载补丁也许可以))相似 //此问题可控,不会有后续风险,所以在此采用后者,但不论哪一种方法,只要忽略Error,均可以运行。 //RemovePlayer(); if(isServer) { RemovePlayer();//先移除player,否则Error:ClientScene::AddPlayer: playerControllerId of 0 already in use. //connectionToClient.Disconnect(); //connectionToClient.Dispose(); LobbyMainMenu.mainMenu.DestroyMatch(); Debug.Log("destroy match"); } else { LobbyMainMenu.mainMenu.DropConnection(); Debug.Log("drop match"); } } else if(isServer)//此处是根据connectionToClient定义做的判断,去掉if(isServer)判断也可以,因为在初始化时已经做了限制,非isserver的不会调用 { //采用Send方法是为了验证消息注册的用法(现在方法简洁明了) //每一个player中的connection(connectionToClient)为客户端连接到主机时建立的 //connectionToClient.Send(playerMsg, new PlayerMsg()); //connectionToClient为服务端与客户端具有相同identity的player的NetworkConnection,即通过一个玩家,客户端与服务端的连接,并且只在server端有效 //所以只要断开连接即可 connectionToClient.Disconnect(); connectionToClient.Dispose(); Debug.Log("disconnect match"); } } public void SendReadyToBeginMessage_() { SendReadyToBeginMessage(); } #endregion #region CALLBACKS public override void OnClientEnterLobby() { base.OnClientEnterLobby(); LobbyMainMenu.mainMenu.AddPlayer(this); if(clientEnterAction!=null) { clientEnterAction(isServer); } //LogInfo.theLogger.Log("Client enter lobby_"+isLocalPlayer+"_"+isServer); LogInfo.theLogger.Log("ClientEnterLobby_"+isClient+"_"+isServer); } public override void OnStartLocalPlayer() { LogInfo.theLogger.Log("StartLocalPlayer_" + isLocalPlayer + "_" + isServer); if(localPlayerAction!=null) { localPlayerAction(isLocalPlayer,isServer); } base.OnStartLocalPlayer(); } public override void OnClientExitLobby() { base.OnClientExitLobby(); } public override void OnClientReady(bool readyState) { LogInfo.theLogger.Log("OnClientReady_" + readyState); base.OnClientReady(readyState); if(clientReadyAction!=null) { clientReadyAction(readyState); } } #endregion class PlayerMsg : MessageBase { } }
using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; public class LobbyPlayerUI : NetworkBehaviour { public LobbyPlayer thePlayer; public Button removeBtn; public Toggle readyBtn; public GameObject readyStateImage; private void OnRemove() { thePlayer.RemovePlayer_(); } private void OnReady(bool isReady) { if (isReady) { thePlayer.SendReadyToBeginMessage(); } else { thePlayer.SendNotReadyToBeginMessage(); } } private void OnClientReady(bool isReady) { RpcOnClientReady(isReady); } [ClientRpc] private void RpcOnClientReady(bool isReady) { readyStateImage.SetActive(isReady); } private void OnClientEnter(bool isServer) { if(!isServer) { //这段是为了对不同的客户端进行权限操作,此处代码完全可以作为初始值,不用设置 removeBtn.GetComponentInChildren<Text>().text = "Leave"; removeBtn.interactable = false; readyBtn.interactable = false; } else { readyBtn.interactable = false; removeBtn.GetComponentInChildren<Text>().text = "Kick"; //通过isLocalPlayer进行主机客户端进行单独设置,但是此时islocalplayer==false(未初始化) //if (isLocalPlayer) //{ // removeBtn.GetComponentInChildren<Text>().text = "Leave"; // readyBtn.interactable = true; //} } } private void LocalPlayerIdentity(bool isLocalPlayer, bool isServer) { if(isLocalPlayer)//此处可以不用判断,因为当create local player时才会调用此方法,而create local player时isLocalPlayer肯定为true { removeBtn.interactable = true; readyBtn.interactable = true; GetComponent<Image>().color = Color.red; if(isServer) removeBtn.GetComponentInChildren<Text>().text = "Leave"; } } private void Awake() { removeBtn.onClick.AddListener(OnRemove); readyBtn.onValueChanged.AddListener(OnReady); thePlayer.clientEnterAction += OnClientEnter; thePlayer.localPlayerAction += LocalPlayerIdentity; thePlayer.clientReadyAction += OnClientReady; } }
using UnityEngine; public class PlayerList : MonoBehaviour { public static PlayerList theList; public void AddPlayer(LobbyPlayer player) { player.transform.SetParent(transform); } private void Start() { theList = this; } }