这两天在做那个CodeMonky的胡闹厨房的案例,一直困扰我的是关于Lobby和Relay的相关网络服务,需要挂加速器并且延迟不低,所以我一直在寻找一些其他替代方案,想起来之前做一个UEC++的网络枪战时做过一个内网穿透的方法,所以在Unity中也采用这个方案,但中间怎么改IP和端口都没法连接成功,最后就索性单开了一个项目来测试这一块,现在也算是测出来原因了,这里写个笔记记录下过程
文章目录
案例概述
首先先说一下写的聊天室案例
使用MessageManager来作为RPC调用,提供全局添加Message方法的单例,其他类可以订阅对应的委托进行自己的处理,比如UI部分可以根据委托创建消息显示
NetworkManager是Unity提供的网络类,配合UnityTransport实现网络的传输和连接的管理
UI部分分三大部分:
- 第一部分是输入消息内容并发送的部分
- 第二部分是提供启动Host和Client的按钮部分
- 第三部分就是消息的生成显示的部分。
关键代码部分
MessageManager
添加消息的RPC部分,该部分只负责转发委托,并不实际存储消息内容,由于一开始是希望方便测试查看网络相关的回调信息,所以添加了一个LogLevel控制消息的颜色,但后来又想不如加个聊天,再后来就成了个聊天室了()
cs
public void AddMessage(string name, string content, LogLevel level = LogLevel.Normal)
{
AddMessageServerRPC(name, content, level);
}
[ServerRpc(RequireOwnership = false)]
private void AddMessageServerRPC(string name, string content, LogLevel level = LogLevel.Normal)
{
AddMessageClientRPC(name, content, level);
}
[ClientRpc]
private void AddMessageClientRPC(string name, string content, LogLevel level = LogLevel.Normal)
{
onMessageReceived.Invoke(name, content, level);
}
接下来是在该类中重新在NetworkManager的StartHost和StartClient外面裹一层自己的代码
cs
public void StartHost()
{
//这一句后面详细解释
NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData("你的公网IP", 7777, "0.0.0.0");
//提示打印玩家的连接通知
//Namer.GM是一个名称类,拿来代替直接使用"GM"的字符串方式,方便统一名称,统一修改,查找引用等
NetworkManager.Singleton.OnClientConnectedCallback += (clientId) =>
{
Instance.AddMessage(Namer.GM, "player " + clientId + " Connected.", LogLevel.Warning);
clientIDList.Add(clientId);
};
NetworkManager.Singleton.StartHost();
}
public void StartClient()
{
NetworkManager.Singleton.OnClientDisconnectCallback += (clientId) =>
{
onMessageReceived.Invoke(Namer.GM, "未能成功连接到服务器!", LogLevel.Error);
};
NetworkManager.Singleton.StartClient();
}
最后是该类中向外部提供的委托
cs
public UnityAction<string, string, LogLevel> onMessageReceived;
MessageUI
接下来是消息的显示部分,这一部分订阅单例的消息委托并显示出来,注意textPrefab是一个网络物体,其父级要在初始化时就提供。并且要在NetworkManager的NetworkPrefabList中添加该Prefab
cs
public class MessageUI : MonoBehaviour
{
[SerializeField] private TextBlockUI textPrefab;
[SerializeField] private Transform textParent;
private void Start()
{
MessageManager.Instance.onMessageReceived += (name, content, level) =>
{
TextBlockUI obj = Instantiate(textPrefab, textParent);
obj.SetText(name, content, level);
};
}
}
SendUI
这一部分算是负责发送消息的部分,本质上就是获取消息然后调用写好的函数就行,另外连接Host和Client只需要两个按钮分别调用MessageManager中对应的StartHost和StartClient就行,之后就不写了
cs
public class SendUI : MonoBehaviour
{
[SerializeField] private TMP_InputField inputField;
[SerializeField] private TMP_Dropdown dropdown;
[SerializeField] private Button submitBtn;
[SerializeField] private TMP_InputField nameField;
// Start is called before the first frame update
void Start()
{
submitBtn.onClick.AddListener(() =>
{
string name = nameField.text;
string content = inputField.text;
LogLevel level = (LogLevel)dropdown.value;
MessageManager.Instance.AddMessage(name, content, level);
inputField.text = "";
});
nameField.onEndEdit.AddListener((str) =>
{
PlayerPrefs.SetString("PlayerName", str);
});
}
}
上述写完后,在本地进行测试,没问题的话就可以准备进行远程连接了
远程连接
远程连接有许多方案,这里说一下我知道的
-
Lobby + Relay : 在国内无服务器,连接靠玄学
-
Unity Online Services:国内的网络服务,相关教程和API可在官网找到,可用,但是免费用户只有五十次房间额度,用完就无了
-
内网穿透:直接连接到玩家主机上,花费视使用的软件而定,最经济的办法是内网穿透做服务器然后放云上,或者像我一样直接用来Host连接,不额外搞服务器
内网穿透:
我使用的是路由侠,穿透时都会分配到一个公网的域名和对应端口,其映射到的内网端口通常选为127.0.0.1 : 7777,IP输入本机IP,后面的7777端口只要选一个没其他进程用的即可
确保穿透是打开的,域名对应的IP可以直接在CMD窗口中输入"ping 公网域名",即可得到IP地址
公网的IP地址和端口填入Unity中NetworkManager下的UnityTransport的Address和Port中,并且打开运行远程连接
到达这一步会发现哪里都正常,但是客户端就是连不上服务器,最主要的一点是监听的端口问题
先打开服务器,再使用CMD,输入netstat -an | find "端口号"
可以得到当前端口的状态,一般是
UDP 0.0.0.0:28907 *:*
,这说明该端口当前监听所有UDP协议的IP地址连接,主机是正常监听的,
客户端也是连接的公网IP和端口,那么唯一的可能就是主机监听的端口和客户端不一致,客户端使用公网IP和端口进行连接,但最后端口会被映射到内网端口上,因此主机应当监听的是内网的对应端口,所以在StartHost时需要修改监听的端口值,也就是MessageManager中StartHost中的第一段代码
cs
NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData("你的公网IP", 7777, "0.0.0.0");
自此大功告成,能够通过内网穿透进行通信。