本文是通过在局域网内进行玩家匹配,需要游戏大厅展示局域网内的服务器列表(房间信息),玩家通过点击列表进入服务器创建的房间,准备好后开始游戏。
由于Unity官方提供NetworkManager HUB只是一个实例,UI丑丑的。而且局域网内匹配也没有可供选择的服务器列表。那服务器在局域网内通过UDP数据传输协议来通知其他客户端生成服务器列表。客户端点击列表进入房间中等待加入游戏。
准备的插件:
1.Network Lobby插件:Asset Store已经下架,我是在下架之前就购买了,所以就导出一份静态资源供大家下载。这个插件主要是让我们的游戏大厅界面好看些。关于Network Lobby插件的教程可以观看此视频,也可以查看Multiplayer and Networking官方文档
2.UnityMainThreadDispatcher插件:子线程中更新UI用的,主要使用在房间列表的更新。如何使用github上有。
首先汉化一下吧,这个不多说。保留 PLAY AND HOST 改成 创建房间。配置LobbyManager
增加UDP脚本,该脚本主要实现在后台运行的接收线程UDP传输信息,需要与NetworkLobby插件结合一下,举个例子当客户端创建房间时,那么就要通过UDP传输告诉其他局域网内的客户端,我创建了一个房间信息。其他客户端在房间列表中加入此信息。看看主要代码。
发送命令代码:这里发送5种消息头,根据不同的消息进行对应处理。
public void sendMessage(string infoHearder, string serverIp = "")
{
UdpClient myUdpClient = new UdpClient();
try
{
//将发送内容转换为字节数组
byte[] bytes = null;
//让其自动提供子网中的IP广播地址
IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, 8001);
string tempStr = "";
string ip = localIp;
if (infoHearder == "create")
{
isServer = true;
bytes = null;
if (serverInfo == null)
{
serverInfo = new mServerInfo();
serverInfo.ip = ip;
serverInfo.status = "0";
serverInfo.currentNum = 1;
}
tempStr = infoHearder + "-" + ip + "-" + ip + "-" + (lobbyManager._playerNumber == 0 ? 1 : lobbyManager._playerNumber);
bytes = Encoding.UTF8.GetBytes(tempStr);
}
else if (infoHearder == "exit")
{
bytes = null;
serverInfo = null;
tempStr = infoHearder + "-" + serverIp + "-" + ip + "-本机退出!";
bytes = Encoding.UTF8.GetBytes(tempStr);
}
else if (infoHearder == "join")
{
bytes = null;
tempStr = infoHearder + "-" + serverIp + "-" + ip + "-客机加入!";
bytes = Encoding.UTF8.GetBytes(tempStr);
}
else if (infoHearder == "kicked")
{
bytes = null;
tempStr = infoHearder + "-" + serverIp + "-" + ip + "-踢了客户端!";
bytes = Encoding.UTF8.GetBytes(tempStr);
}
else if (infoHearder == "start")
{
bytes = null;
tempStr = infoHearder + "-" + serverIp + "-" + ip + "-服务器开始了!";
bytes = Encoding.UTF8.GetBytes(tempStr);
}
//向子网发送信息
myUdpClient.Send(bytes, bytes.Length, iep);
Debug.Log("send " + tempStr);
}
catch (Exception err)
{
Debug.Log("发送失败" + err.Message);
}
finally
{
myUdpClient.Close();
}
}
创建接收信息的线程:
void Start()
{
monitorThread = new Thread(ReceiveData);
//将线程设为后台运行
monitorThread.IsBackground = true;
monitorThread.Start();
}
接收信息及更新UI的代码:这里包涵接收5种消息头所对应的的处理方式。
///
/// 在后台运行的接收线程
///
private void ReceiveData()
{
//在本机指定的端口接收
udpClient = new UdpClient(port);
IPEndPoint remote = null;
//接收从远程主机发送过来的信息;
while (true)
{
try
{
//关闭udpClient时此句会产生异常
byte[] bytes = udpClient.Receive(ref remote);
str = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
Debug.Log("rec-" + str);
Debug.Log("远程主机IP-" + remote.ToString());
//更新UI
UnityMainThreadDispatcher.Instance().Enqueue(ThisWillBeExecutedOnTheMainThread(str, remote));
}
catch (Exception ex)
{
Debug.Log("rec-" + ex.Message);
//退出循环,结束线程
break;
}
}
}
public IEnumerator ThisWillBeExecutedOnTheMainThread(string str, IPEndPoint remote)
{
try
{
sendMessageText.text = str;
status.text = "";
var arr = str.Split('-');
string headMes = arr[0];
if (headMes == "create")
{
var tempIp = remote.ToString().Split(':')[0];
if (!serverList.Exists(p => p.ip == tempIp))
{
mServerInfo tempInfo = new mServerInfo();
tempInfo.status = "0";
tempInfo.ip = tempIp;
tempInfo.num = 5;
tempInfo.currentNum = Convert.ToInt32(arr[arr.Length - 1]);
serverList.Add(tempInfo);
GameObject _roomListItemGo = Instantiate(roomItemPrefabs);
_roomListItemGo.GetComponent
在上面的代码中,在create类型的消息头中进行房间列表的更新,LobbyMainMenu主要代码:roomItemPrefabs是房间信息的预制体,roomContent是ScrollView中的内容, roomList添加roomItemPrefabs为了ScrollView添加、移除操作的进行。这里顺便把加入房间按钮事件也注册了。
GameObject _roomListItemGo = Instantiate(roomItemPrefabs);
_roomListItemGo.GetComponent
在原来LobbyMainMenu脚本中的改写OnClickJoin方法添加一些加入的规则(人数的判断,房间的状态等),我这里新增了OnListClickJoin并进行传参,原因是要获取单击按钮上的server ip使客户端能加入房间。同时发送join的消息头及获取到的server ip,使用udp进行局域网内广播那么在其他客户端接收到join消息头时,可以通过传输的 server ip进行判断哪个客户端游戏的服务器。这样就通过server ip更新列表的人数信息。
public void OnListClickJoin(GameObject myGO)
{
var list = udp.getServerList();
var textIp = myGO.transform.Find("ip").GetComponent().text;
Debug.Log(textIp);
if (!list.Exists(p => p.ip == textIp))
{
lobbyManager.SetServerInfo("主机信息不存在!", lobbyManager.networkAddress);
return;
}
else
{
var server = list.Find(p => p.ip == textIp);
if (server.status == "1")
{
lobbyManager.infoPanel.Display("房间已经开始,不能加入!", "确定",null);
return;
}
else if (server.status == "2")
{
lobbyManager.infoPanel.Display("房间人数已经够了,不能加入!", "确定", null);
return;
}
else if (server.status == "-1")
{
lobbyManager.infoPanel.Display("房间不能加入!", "确定", null);
return;
}
}
lobbyManager.ChangeTo(lobbyPanel);
lobbyManager.networkAddress = textIp;
lobbyManager.StartClient();
lobbyManager.backDelegate = lobbyManager.StopClientClbk;
lobbyManager.DisplayIsConnecting();
lobbyManager.SetServerInfo("正在连接...", lobbyManager.networkAddress);
udp.sendMessage("join", textIp);
}
在ReceiveData脚本中join消息头增加人数。
if (headMes == "join")
{
serverIp = arr[1];
Debug.Log("join:" + serverIp);
for (int i = 0; i < serverList.Count; i++)
{
var temp = serverList[i];
if (temp.ip == serverIp)
{
temp.currentNum += 1;
var _roomListItemGo = roomList[i];
_roomListItemGo.transform.Find("rs").GetComponent().text = "(" + temp.currentNum + "/" + temp.num + ")";
if (temp.currentNum == temp.num)
{
temp.status = "2";
}
}
}
}
在客户端/服务器退出时,需要进行udp消息头的发送。修改LobbyManager脚本中的GoBackButton方法,加入 udp.sendMessage("exit", udp.serverIp)
public delegate void BackButtonDelegate();
public BackButtonDelegate backDelegate;
public void GoBackButton()
{
backDelegate();
topPanel.isInGame = false;
udp.sendMessage("exit", udp.serverIp);
if (mainMenu.serverThread != null)
{
mainMenu.serverThread.Abort();
mainMenu.serverThread = null;
}
}
在局域网内接收到exit的消息头时,如果是服务器退出时需要移除房间列表中的信息,客户端退出时需要修改列表中的人数信息。那么在ReceiveData接收信息后就需要针对性的处理。这里的remote是udp发送消息的IPEndPoint信息。
if (headMes == "exit")
{
serverIp = arr[1];
string ip = remote.ToString().Split(':')[0];
for (int i = 0; i < serverList.Count; i++)
{
if (serverList[i].ip == ip)
{
serverList.RemoveAt(i);
//列表移除
Destroy(roomList[i]);
roomList.RemoveAt(i);
}
if (!isServer)
{
var temp = serverList[i];
if (temp.ip == serverIp)
{
temp.currentNum -= 1;
temp.currentNum = temp.currentNum < 1 ? 1 : temp.currentNum;
var _roomListItemGo = roomList[i];
_roomListItemGo.transform.Find("rs").GetComponent().text = "(" + temp.currentNum + "/" + temp.num + ")";
if (temp.currentNum < temp.num && temp.status != "1")
{
temp.status = "0";
}
}
}
}
ipText.text = ip;
}
kicked消息头的发送与处理:这个命令是客户端被房主提出房间。修改LobbyManager脚本中的KickedMessageHandler函数增加发送kicked消息头的命令。
public void KickedMessageHandler(NetworkMessage netMsg)
{
udp.sendMessage("kicked");
infoPanel.Display("房主把你请出房间", "关闭", null);
netMsg.conn.Disconnect();
}
当我在做完这些工作后遇到一个问题:就是如何将用户列表选择文本信息和颜色显示在游戏预制体上呢,网上找了好些资料终于在这个插件的脚本中发现了一个LobbyHook的脚本。在这里看看因为注解就明白了,然后又搜索了一下官方文档关于OnLobbyServerSceneLoadedForPlayer 的说明。
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using UnityEngine.UI;
namespace Prototype.NetworkLobby
{
// Subclass this and redefine the function you want
// then add it to the lobby prefab
public abstract class LobbyHook : MonoBehaviour
{
public virtual void OnLobbyServerSceneLoadedForPlayer(NetworkManager manager, GameObject lobbyPlayer, GameObject gamePlayer) {
//gamePlayer.transform.Find("nameCvs/playerName").GetComponent().text = "2222";
}
}
}
那么我们新建一个脚本myLobbyHook继承LobbyHook脚本。重写此方法:
public override void OnLobbyServerSceneLoadedForPlayer(NetworkManager manager, GameObject lobbyPlayer, GameObject gamePlayer)
{
TankNameController tankName = gamePlayer.GetComponent();
tankName.playName = lobbyPlayer.GetComponent().playerDropName;
tankName.PlaryColor = lobbyPlayer.GetComponent().playerColor;
}
在这里需要注意下官方有个说明这个方法是运行在服务器端的,也就是说还涉及到文本信息及选择的颜色同步到其他客户端的问题。上代码
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class TankNameController : NetworkBehaviour
{
public Text name;
[SyncVar]
public string playName;
[SyncVar]
public Color PlaryColor = Color.white;
void Update()
{
name.color = PlaryColor;
name.text = playName;
}
}