文章目录
一、前言二、简单的Socket通信:多人聊天室1、服务端:python代码1.1、import socket1.2、构造socket对象1.3、绑定/监听端口1.3、监听客户端连接1.4、接收客户端socket消息1.5、多线程1.6、完整代码:game_server.py2、客户端:Unity2.1、创建工程,搭建场景2.2、Socket封装:ClientSocket.cs2.2.1、构造Socket对象2.2.2、连接服务器2.2.3、断开连接2.2.4、发送消息2.2.5、接收服务端消息2.2.6、完整代码:ClientSocket.cs2.3、UI交互:TestPanel.cs2.3.1、定义变量2.3.2、登录服务端2.3.3、断开连接2.3.4、发送消息2.3.5、接收消息2.3.6、完整代码:TestPanel.cs2.4、挂脚本,赋值成员对象3、打包客户端4、运行测试5、工程源码三、拓展:Mirror Networking1、局域网多人联机Demo的救星:Mirror2、关于Mirror3、Mirror插件下载4、Mirror 案例测试:多人坦克对战5、Mirror 案例讲解:多人坦克对战5.1、NetworkManager物体5.1.1、NetworkManager组件5.1.2、NetworkManagerHUD组件5.1.3、KcpTransport组件5.2、地面(带导航功能)5.2.1、创建Plane5.2.2、导航烘焙:Navigation5.3、坦克生成点:NewworkStartPosition5.4、坦克身上的组件5.4.1、坦克预设5.4.2、NavMeshAgent组件5.4.4、Animator组件5.4.5、NetworkTransform组件5.4.6、NetworkIdentity组件5.4.7、NetworkBehaviour组件: Tank5.5、赋值PlayerPrefab5.6、炮弹预设5.7、坦克脚本:Tank.cs5.8、Transform的网络同步:NetworkTransform.cs5.9、炮弹脚本:Projectile.cs四、完毕一、前言
嗨,大家好,我是新发。
事情是这样的,上次有同学问我能不能出一期网络相关的教程,
然而我眼花看错了,看成了网格,我还专门写了一篇文章:《【游戏开发进阶】Unity网格探险之旅(Mesh | 动态合批 | 骨骼动画 | 蒙皮 )》
直到有同学在评论里提醒我,真是尴尬…
嘛,没事,今天就补上,写一篇网络相关文章。
我准备做个例子,使用.Net
原生的Socket
模块来实现简单的多人聊天室功能。
话不多说,我们开始吧~
二、简单的Socket通信:多人聊天室
Unity
中我们要实现网络通信,可以使用.Net
的Socket
模块来实现。
为了演示,我就用python
写个简单的服务端,用Unity
作为客户端。
先画个流程图。
服务端(python
)流程图:
客户端(Unity
)流程图:
1、服务端:python代码
新建一个python
脚本:game_server.py
,如下
1.1、import socket
因为我们要使用socket
,所以先引入socket
模块:
import socket
1.2、构造socket对象
g_socket_server = Noneg_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
关于socket
的python
函数原型可以使用help(socket)
查看,
第一个参数是socket domains
(通信协议族),有两种类型:AF_UNIX
、AF_INET
,它们的区别:
第二个参数是socket type
(套接字类型),有SOCKET_STREAM
、SOCK_DGRAM
、SOCK_RAW
三种,
1.3、绑定/监听端口
ADDRESS = ('127.0.0.1', 8712)g_socket_server.bind(ADDRESS)g_socket_server.listen(5)
1.3、监听客户端连接
client, info = g_socket_server.accept()
1.4、接收客户端socket消息
data = client.recv(1024)msg = data.decode(encoding='utf8')
使用json
对消息字段进行解析:
import jsonjd = json.loads(jsonstr)protocol = jd['protocol']uname = jd['uname']msg = jd['msg']
1.5、多线程
由于监听客户端(socket.accept
)和接收消息(socket.recv
)都是阻塞的,为了不阻塞主线程,我们使用子线程来处理。
创建不带参数的线程:
thread = Thread(target=thread_func)thread.start()def thread_func():pass
创建带参数的线程:
thread = Thread(target=thread_func, args=(p1, p2, p3))thread.start()def thread_func(p1, p2, p3):pass
1.6、完整代码:game_server.py
最终,game_server.py
完整代码如下:
'''作者:林新发,博客:/linxinfa功能:简单的Socket通信,聊天室服务端python版本:3.6.4'''import socket # 导入 socket 模块from threading import Threadimport timeimport jsonADDRESS = ('127.0.0.1', 8712) # 绑定地址g_socket_server = None # 负责监听的socketg_conn_pool = {} # 连接池def accept_client():global g_socket_serverg_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) g_socket_server.bind(ADDRESS)g_socket_server.listen(5) # 最大等待数(有很多人理解为最大连接数,其实是错误的)print("server start,wait for client connecting...")'''接收新连接'''while True:client, info = g_socket_server.accept() # 阻塞,等待客户端连接# 给每个客户端创建一个独立的线程进行管理thread = Thread(target=message_handle, args=(client, info))thread.setDaemon(True)thread.start()def message_handle(client, info):'''消息处理'''handle_id = info[1]# 缓存客户端socket对象g_conn_pool[handle_id] = clientwhile True:try:data = client.recv(1024)jsonstr = data.decode(encoding='utf8')jd = json.loads(jsonstr)protocol = jd['protocol']uname = jd['uname']if 'login' == protocol:print('on client login, ' + uname)# 转发给所有客户端for u in g_conn_pool:g_conn_pool[u].sendall((uname + " 进入了房间").encode(encoding='utf8'))elif 'chat' == protocol:# 收到客户端聊天消息print(uname + ":" + jd['msg'])# 转发给所有客户端for key in g_conn_pool:g_conn_pool[key].sendall((uname + " : " + jd['msg']).encode(encoding='utf8'))except Exception as e:remove_client(handle_id)breakdef remove_client(handle_id):client = g_conn_pool[handle_id]if None != client:client.close()g_conn_pool.pop(handle_id)print("client offline: " + str(handle_id))if __name__ == '__main__':# 新开一个线程,用于接收新连接thread = Thread(target=accept_client)thread.setDaemon(True)thread.start()# 主线程逻辑while True:time.sleep(0.1)
2、客户端:Unity
2.1、创建工程,搭建场景
新建一个Unity
工程,
使用UGUI
简单搭建一下界面,如下
养成好习惯,界面保存为预设:TestPanel.prefab
,
2.2、Socket封装:ClientSocket.cs
我们先封装一个ClientSocket.cs
,实现Socket
的创建、连接和收发消息等功能。
2.2.1、构造Socket对象
// using .Sockets;Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2.2.2、连接服务器
socket.Connect(host, port);
2.2.3、断开连接
socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;
2.2.4、发送消息
// byte[] bytes 你的消息的字节数组NetworkStream netstream = new NetworkStream(socket);netstream.Write(bytes, 0, bytes.Length);
2.2.5、接收服务端消息
// 回调函数对象AsyncCallback recvCb = new AsyncCallback(RecvCallBack);// 数据缓存byte[] recvBuff = new byte[0x4000];// 消息队列Queue<string> msgQueue = new Queue<string>();// 每帧调用此方法socket.BeginReceive(recvBuff, 0, recvBuff.Length, SocketFlags.None, recvCb, this);// 接收消息回调函数private void RecvCallBack(IAsyncResult ar){var len = socket.EndReceive(ar);byte[] msg = new byte[len];Array.Copy(m_recvBuff, msg, len);var msgStr = System.Text.Encoding.UTF8.GetString(msg);// 将消息塞入队列中msgQueue.Enqueue(msgStr);}// 从消息队列中取出消息(供外部调用)public string GetMsgFromQueue(){if (msgQueue.Count > 0)return msgQueue.Dequeue();return null;}
2.2.6、完整代码:ClientSocket.cs
最终,ClientSocket.cs
完整代码如下:
/** Socket封装* 作者:林新发 博客:/linxinfa*/using System;using .Sockets;using UnityEngine;using System.Collections.Generic;public class ClientSocket{private Socket init(){Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// 接收的消息数据包大小限制为 0x4000 byte, 即16KBm_recvBuff = new byte[0x4000];m_recvCb = new AsyncCallback(RecvCallBack);return clientSocket;}/// <summary>/// 连接服务器/// </summary>/// <param name="host">ip地址</param>/// <param name="port">端口号</param>public void Connect(string host, int port){if (m_socket == null)m_socket = init();try{Debug.Log("connect: " + host + ":" + port);m_socket.SendTimeout = 3;m_socket.Connect(host, port);connected = true;}catch (Exception ex){Debug.LogError(ex);}}/// <summary>/// 发送消息/// </summary>public void SendData(byte[] bytes){NetworkStream netstream = new NetworkStream(m_socket);netstream.Write(bytes, 0, bytes.Length);}/// <summary>/// 尝试接收消息(每帧调用)/// </summary>public void BeginReceive(){m_socket.BeginReceive(m_recvBuff, 0, m_recvBuff.Length, SocketFlags.None, m_recvCb, this);}/// <summary>/// 当收到服务器的消息时会回调这个函数/// </summary>private void RecvCallBack(IAsyncResult ar){var len = m_socket.EndReceive(ar);byte[] msg = new byte[len];Array.Copy(m_recvBuff, msg, len);var msgStr = System.Text.Encoding.UTF8.GetString(msg);// 将消息塞入队列中m_msgQueue.Enqueue(msgStr);// 将buffer清零for (int i = 0; i < m_recvBuff.Length; ++i){m_recvBuff[i] = 0;}}/// <summary>/// 从消息队列中取出消息/// </summary>/// <returns></returns>public string GetMsgFromQueue(){if (m_msgQueue.Count > 0)return m_msgQueue.Dequeue();return null;}/// <summary>/// 关闭Socket/// </summary>public void CloseSocket(){Debug.Log("close socket");try{m_socket.Shutdown(SocketShutdown.Both);m_socket.Close();}catch(Exception e){//Debug.LogError(e);}finally{m_socket = null;connected = false;}}public bool connected = false;private byte[] m_recvBuff;private AsyncCallback m_recvCb;private Queue<string> m_msgQueue = new Queue<string>();private Socket m_socket;}
2.3、UI交互:TestPanel.cs
然后再创建一个脚本:TestPanel.cs
,用于实现UI
部分的交互逻辑。
2.3.1、定义变量
先定义一些变量:
private const string IP = "127.0.0.1";private const int PORT = 8712;// 用户名输入public InputField unameInput;// 消息输入public InputField msgInput;// 登录按钮public Button loginBtn;// 发送按钮public Button sendBtn;// 连接状态文本public Text stateTxt;// 连接按钮文本public Text connectBtnText;// 聊天室聊天文本public Text chatMsgTxt;// 封装的ClientSocket对象private ClientSocket clientSocket = new ClientSocket();
2.3.2、登录服务端
// 连接clientSocket.Connect(IP, PORT);stateTxt.text = clientSocket.connected ? "已连接" : "未连接";connectBtnText.text = clientSocket.connected ? "断开" : "连接";if (clientSocket.connected)unameInput.enabled = false;// 登录Send("login");
2.3.3、断开连接
clientSocket.CloseSocket();stateTxt.text = "已断开";connectBtnText.text = "连接";unameInput.enabled = true;
2.3.4、发送消息
这里用了一个迷你版的
json
库:JSONConvert
,源码可以参见我之前写的这篇文章:《用C#实现一个迷你json库,无需引入dll(可直接放到Unity中使用)》
private void Send(string protocol, string msg = ""){JSONObject jsonObj = new JSONObject();jsonObj["protocol"] = protocol;jsonObj["uname"] = unameInput.text;jsonObj["msg"] = msg;// JSONObject转stringstring jsonStr = JSONConvert.SerializeObject(jsonObj);// string转byte[]byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonStr);// 发送消息给服务端clientSocket.SendData(data);}
2.3.5、接收消息
private void Update(){if (clientSocket.connected){clientSocket.BeginReceive();}var msg = clientSocket.GetMsgFromQueue();if (!string.IsNullOrEmpty(msg)){// 显示到聊天室文本中chatMsgTxt.text += msg + "\n";Debug.Log("RecvCallBack: " + msg);}}
2.3.6、完整代码:TestPanel.cs
最终,TestPanel.cs
完整代码如下:
/** 聊天室客户端 UI交互* 作者:林新发 博客:/linxinfa*/using UnityEngine;using UnityEngine.UI;public class TestPanel : MonoBehaviour{private const string IP = "127.0.0.1";private const int PORT = 8712;// 用户名输入public InputField unameInput;// 消息输入public InputField msgInput;// 登录按钮public Button loginBtn;// 发送按钮public Button sendBtn;// 连接状态文本public Text stateTxt;// 连接按钮文本public Text connectBtnText;// 聊天室聊天文本public Text chatMsgTxt;// 封装的ClientSocket对象private ClientSocket clientSocket = new ClientSocket();private ClientSocket clientSocket = new ClientSocket();void Start(){chatMsgTxt.text = "";loginBtn.onClick.AddListener(() =>{if (clientSocket.connected){// 断开clientSocket.CloseSocket();stateTxt.text = "已断开";connectBtnText.text = "连接";unameInput.enabled = true;}else{// 连接clientSocket.Connect(IP, PORT);stateTxt.text = clientSocket.connected ? "已连接" : "未连接";connectBtnText.text = clientSocket.connected ? "断开" : "连接";if (clientSocket.connected)unameInput.enabled = false;// 登录Send("login");}});sendBtn.onClick.AddListener(() =>{Send("chat", msgInput.text);});}private void Update(){if (clientSocket.connected){clientSocket.BeginReceive();}var msg = clientSocket.GetMsgFromQueue();if (!string.IsNullOrEmpty(msg)){chatMsgTxt.SetAllDirty();chatMsgTxt.text += msg + "\n";Debug.Log("RecvCallBack: " + msg);}}private void Send(string protocol, string msg = ""){JSONObject jsonObj = new JSONObject();jsonObj["protocol"] = protocol;jsonObj["uname"] = unameInput.text;jsonObj["msg"] = msg;// JSONObject转stringstring jsonStr = JSONConvert.SerializeObject(jsonObj);// string转byte[]byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonStr);// 发送消息给服务端clientSocket.SendData(data);}private void OnApplicationQuit(){if (clientSocket.connected){clientSocket.CloseSocket();}}}
2.4、挂脚本,赋值成员对象
给TestPanel
界面挂上TestPanel.cs
脚本,赋值成员对象,如下
3、打包客户端
因为我们要测试多个客户端连接一个服务端,为了方便测试,我们打个Windows
平台的exe
。
在Build Settings
中添加要打包的场景,选择PC, Mac & Linux Standalone
平台,
我们不想全屏显示客户端,在Player Settings
中,找到Resolution and Presentation
,设置Fullscreen Mode
为Windowed
,设置窗口默认宽高为640 x 360
,
执行打包,
打包成功,
4、运行测试
先使用python
运行服务端,
开启多个客户端,分别登录服务端,用户名分别是皮皮猫
和林新发
吧~
服务端的输出:
开始聊天,
服务端的输出:
运行一切正常,完美。
5、工程源码
上面这个简单聊天室工程源码已上传到CODE CHINA
,感兴趣的同学可自行下载下来进行学习,
工程地址:/linxinfa/UnitySocketDemo
注:我使用的Unity
版本:Unity .1.9f1c1 (64-bit)
。
另外关于
CODE CHINA
的使用教程我之前也写了一篇文章,感兴趣的同学可以看看:《CODE.CHINA使用教程,创建项目仓库并上传代码(git)》
三、拓展:Mirror Networking
1、局域网多人联机Demo的救星:Mirror
上面的简单聊天室功能,我们是做了一个独立的服务端负责消息的转发,聊天本身的逻辑非常简单,我们把大部分工作花在了维护Socket
上,要解决多线程问题,要解决连接断开,要解决消息的序列化和反序列化等等。
有些同学做了一个单机版的小Demo
,想改成局域网多人联机版,要处理好多复杂的同步问题,比如物理碰撞、状态同步等等,这个对于Unity
萌新来说,不大友好。
有没有什么好用的网络库可以让开发更高效呢?有,那就是:Mirror
!
注:在
Unity 5.1 ~ Unity
中你可以使用UNet
(全称Unity Networking
),到Unity
之后UNet
就被废弃了,Mirror
就是来替代UNet
的。你在网上搜到的Unity Netwoking
的教程就是UNet
,它已经过时了,不要再使用UNet
了!
2、关于Mirror
Mirror
是Unity
的高级网络API
,支持不同的低级传输(UDP
、TCP
、KCP
等等)。
使用Mirror
,客户端、服务端是在同一个工程中的,这就是为什么它叫Mirror
。也就是说它没有一个独立的服务端,而是由一台客户端作为Host
,它既是客户端又是服务端,其他客户端连接这台Host
客户端。画成图是这样子:
Mirror
是开源的,它的社区很活跃,配套的文档也很详尽,大家可以从官网进行学习,不过是全英文的。
Mirror官网:
https://mirror-/
Mirror GitHub:
/vis2k/Mirror
Mirror Asset Store:
/packages/tools/network/mirror-129321
Mirror 官方文档:
https://mirror-networking.gitbook.io/docs/
Mirror API手册:
https://mirror-/docs/api/Mirror.html
Unity 与 Mirror的兼容:
Mirror
最适合Unity LTS
。
Mirror
通常也适用于所有较新的LTS
版本(即 LTS
)。
3、Mirror插件下载
建议从Asset Store
上下载Mirror
版本,因为GitHub
的版本不一定稳定,
Asset Store
地址:
/packages/tools/network/mirror-129321
将Mirror
插件添加到自己的账号中,然后回到Unity
,在Package Manager
中就可以下载了,
下载下来导入Unity
中,
4、Mirror 案例测试:多人坦克对战
Mirror
中给我们提供了几个例子,
我以多人坦克对战为例,双击Assets / Mirror / Examples / Tanks / Scenes/ Scene
进入场景,
运行后左上角出现三个按钮,如下
要开启两个客户端,为了方便演示,我先打出个exe
,
打包成功后,运行两个客户端,其中一个作为Host
,另一个客户端连接Host
,运行效果如下:
可以看到我们对坦克的控制是实时同步到另一个端的。
5、Mirror 案例讲解:多人坦克对战
下面,我以多人坦克对战案例为例,给大家讲下制作过程。
为了让大家有个直观理解,我画个图:
5.1、NetworkManager物体
先创建一个空物体,重命名为NetworkManager
,挂以下三个脚本:
NetworkManager
、NetworkManagerHUD
、KcpTransport
,
5.1.1、NetworkManager组件
我们先看下官方手册:https://mirror-networking.gitbook.io/docs/components/network-manager
意思就是,NetworkManager
是管理多个客户端连接的组件。它是多人联机游戏的核心控制组件。
一个场景中只能有一个激活的NetworkManager
(它是单例模式的)。
连接的服务端IP
地址在NetworkManager
中进行设置,Max Connections
是最大连接数。
(注意:任何一个客户端都可以同时是一个服务端)
5.1.2、NetworkManagerHUD组件
NetworkManagerHUD
组件是下面这个GUI
的逻辑,通过它我们可以方便地进行测试。
5.1.3、KcpTransport组件
Mirror
帮我们封装了各种不同等级的传输协议(各种Transport
组件),常用的是KcpTransport
和TelepathyTransport
。
KcpTransport
是使用可靠UDP
协议,TelepathyTransport
是使用TCP
协议。
Transport
组件中可以设置端口号、最大延迟等等参数:
5.2、地面(带导航功能)
5.2.1、创建Plane
创建一个Plane
作为地面地面,重命名为Ground
,给它赋值一个材质球,
效果如下:
5.2.2、导航烘焙:Navigation
接下来我们对地面执行导航系统烘焙,这样方便限制坦克的活动范围。
我们将地面设置为静态对象,
点击菜单Window / AI / Navigation
,打开Navigation
(导航/寻路系统)视图,
在Navigation
视图中点击Bake
标签按钮,点击Bake
按钮,对地面进行导航烘焙,
看到蓝色网格则说明烘焙成功,
5.3、坦克生成点:NewworkStartPosition
创建四个空物体,重名命为Spawn
,挂上NewworkStartPosition
,
注:如果不创建生成点,则坦克默认在
(0, 0, 0)
坐标点出生成。
调节四个生成点的位置,分散在地面的四个角落,如下
5.4、坦克身上的组件
5.4.1、坦克预设
准备一个坦克模型,
包装成坦克预设:Tank.prefab
,
坦克预设上挂以下脚本:
5.4.2、NavMeshAgent组件
NavMeshAgent
组件是导航代理组件,挂上这个组件就具备了导航功能;
关于导航系统的使用,可以参见我之前写的文章:《Unity游戏开发——新发教你做游戏(五):导航系统Navigation》
《[原创] 用Unity等比例制作广州地铁,广州加油,早日战胜疫情(Unity | 地铁地图 | 第三人称视角)》
5.4.4、Animator组件
动画控制器,用于控制坦克的行驶、开炮等动画。
关于Animator
相关的教程,我之前写过两篇文章:《Unity动画状态机Animator使用》、
《Animator控制角色动画播放》,感兴趣的同学可以看看。
5.4.5、NetworkTransform组件
我们先看下官方手册:https://mirror-networking.gitbook.io/docs/components/network-transform
意思就是说,NetworkTransform
组件会通过网络自动同步position
、rotation
和scale
。
带NetworkTransform
组件的物体必须也带NetworkIdentity
组件。
我们可以设置Positon
、Rotation
、Scale
同步的敏感度,
为了让同步有一个平滑效果(不会一卡一卡的),我们可以勾选平滑差值,
5.4.6、NetworkIdentity组件
我们先看下官方手册:https://mirror-networking.gitbook.io/docs/components/network-identity
意思就是说,NetworkIdentity
组件提供了游戏物体在网络中的唯一标识(ID
)。
游戏运行过程中,我们在Inspector
视图中预览到NetworkIdentity
的信息。
5.4.7、NetworkBehaviour组件: Tank
Tank
脚本是坦克行为脚本,它继承NetworkBehaviour
。
这里只讲NetworkBehaviour
组件,Tank
具体代码后面再讲~
我们先看看官方手册:https://mirror-networking.gitbook.io/docs/guides/networkbehaviour
意思就是说,NetworkBehaviour
脚本处理具有NetworkIdentity
组件的游戏对象,NetworkBehaviour
的子类中可以处理高级API
功能,例如Commands
、ClientRpc's
、SyncEvents
、SyncVars
。
NetworkBehaviour组件具有以下功能:
Synchronized variables
:同步变量
Network callbacks
:网络回调
Server and client functions
:服务端和客户端函数
Sending commands
:发送命令
Client RPC calls
:客户端远程过程调用
Networked events
:网络事件
NetworkBehaviour
提供了一些网络回调:OnStartServer回调
这个回调函数只在服务端调用,当在服务端生成一个游戏对象,或者服务端启动时被回调。
OnStopServer回调
这个回调函数只在服务端调用,当在服务端销毁一个游戏对象,或者服务端停止时被回调。
OnStartClient回调
这个回调函数只在客户端调用,当客户端生成一个游戏对象,或者客户端连接到服务端时被回调。
OnStopClient回调
这个回调函数只在客户端调用,当服务端销毁一个游戏对象时被回调。
OnStartLocalPlayer回调
这个回调函数只在客户端调用,当客户端生成一个玩家对象时被回调。
OnStartAuthority回调
这个回调函数只在客户端调用,当游戏对象拿到控制权时。
OnStopAuthority回调
这个回调函数只在客户端调用,当游戏对象失去控制权时。
标记服务端函数或客户端函数:
在
NetworkBehaviour
中,我们可以使用[Server]
、[ServerCallback]
、[Client]
、[ClientCallback]
这些注解对函数进行标注。
[Server]
、[ServerCallback]
表示函数为服务端函数,只在服务端执行;
[Client]
、[ClientCallback]
表示为客户端函数,只在客户端执行。
Command 命令:
使用
[Command]
注解对函数进行标记,表示这个函数是由客户端调用,由服务端来执行。具体原理我下文会通过反编译dll
来解释。被
[Command]
标记的函数约定以Cmd
开头。
Client RPC 客户端远程过程调用:
使用
[ClientRpc]
注解对函数进行标记,表示这个函数是由服务端调用,由客户端来执行。具体原理我下文会通过反编译dll
来解释。被
[ClientRpc]
标记的函数约定以Rpc
开头。
Networked Events 网络事件(观察者模式):
类似于
Client RPC
调用,不同之处是它触发的是事件。使用
[SyncEvent]
对事件进行标记。被[SyncEvent]
标记的事件变量必须以Event
开头,例EventTakeDamage
。例子可以参见官方手册:https://mirror-networking.gitbook.io/docs/guides/synchronization/syncevent
Mirror
提供的函数注解如下(部分注解我们上面已做了介绍),具体的注解可以参见Mirror
官方手册:https://mirror-networking.gitbook.io/docs/guides/attributes
5.5、赋值PlayerPrefab
选中NetworkManager
物体,给NetworkManager
组件赋值PlayerPrefab
为坦克预设,
5.6、炮弹预设
准备一个炮弹模型,
包装成炮弹预设:Projectile.prefab
,
炮弹预设上挂以下脚本:
NetworkIdentity
:因为炮弹也是一个网络对象,所以它需要NetworkIdentity
组件;
炮弹的Transform
信息不使用NetworkTransform
进行同步,而是通过Rigibody
刚体组件的力来使炮弹飞行,所以只需要同步一下力即可,在Projectile
脚本中实现炮弹的逻辑。
5.7、坦克脚本:Tank.cs
网络对象的行为脚本需要继承NetworkBehaviour
,所以Tank
类需要继承NetworkBehaviour
,
public class Tank : NetworkBehaviour{}
Tank
脚本要实现的逻辑是坦克的移动 / 旋转、开炮。
其中移动的同步会自动通过NetworkTransform
进行同步,所以我们只需对本地坦克进行控制即可,
// Tank.csvoid Update(){// isLocalPlayer是父类NetworkBehaviour的属性,用于判断当前NetworkBehaviour对象是否为本地对象;if (!isLocalPlayer) return;// 旋转float horizontal = Input.GetAxis("Horizontal");transform.Rotate(0, horizontal * rotationSpeed * Time.deltaTime, 0);// 移动float vertical = Input.GetAxis("Vertical");Vector3 forward = transform.TransformDirection(Vector3.forward);agent.velocity = forward * Mathf.Max(vertical, 0) * agent.speed;animator.SetBool("Moving", agent.velocity != Vector3.zero);// ...}
开炮需要由服务端来执行,
// Tank.csvoid Update(){// ...if (Input.GetKeyDown(shootKey)){CmdFire();}}// this is called on the server[Command]void CmdFire(){GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, transform.rotation);NetworkServer.Spawn(projectile);RpcOnFire();}// this is called on the tank that fired for all observers[ClientRpc]void RpcOnFire(){animator.SetTrigger("Shoot");}
这里用到了两个注解[Command]
、[ClientRpc]
,我们上面讲到它是NetworkBehaviour
组件的函数注解。
上面我们讲到[Command]
,它是由客户端来调用,由服务端来执行。
这个怎么理解呢?
事实上Mirror
实现了一些编译器hack
,会在编译阶段动态生成特定的代码(也就是把你的代码编译为别的代码)。
这样讲好像不好理解,没事,我们反编译一下C#
的dll
就知道了。
进入工程路径 / Library / ScriptAssemblies
这个目录,Mirror
的案例代码是编译在Mirror.Examples.dll
中,
我们使用ILSpy.exe
对它进行反编译,
注:
ILSpy
反编译工具可以从GitHub
下载:/icsharpcode/ILSpy
我们看到反编译出来的Tank
的CmdFire
函数的代码已经完全变了另外一个逻辑了,它发送了一个“CmdFire”
消息给服务端,
开炮流程变成了下面这样子:
同理,[ClientRpc]
是由服务端调用,由客户端执行。
我们的代码:
编译后:
完整的Tank.cs
代码入下:
using UnityEngine;using UnityEngine.AI;namespace Mirror.Examples.Tanks{public class Tank : NetworkBehaviour{[Header("Components")]public NavMeshAgent agent;public Animator animator;[Header("Movement")]public float rotationSpeed = 100;[Header("Firing")]public KeyCode shootKey = KeyCode.Space;public GameObject projectilePrefab;public Transform projectileMount;void Update(){// movement for local playerif (!isLocalPlayer) return;// rotatefloat horizontal = Input.GetAxis("Horizontal");transform.Rotate(0, horizontal * rotationSpeed * Time.deltaTime, 0);// movefloat vertical = Input.GetAxis("Vertical");Vector3 forward = transform.TransformDirection(Vector3.forward);agent.velocity = forward * Mathf.Max(vertical, 0) * agent.speed;animator.SetBool("Moving", agent.velocity != Vector3.zero);// shootif (Input.GetKeyDown(shootKey)){CmdFire();}}// this is called on the server[Command]void CmdFire(){GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, transform.rotation);NetworkServer.Spawn(projectile);RpcOnFire();}// this is called on the tank that fired for all observers[ClientRpc]void RpcOnFire(){animator.SetTrigger("Shoot");}}}
5.8、Transform的网络同步:NetworkTransform.cs
坦克身上挂NetworkTransform
组件,坦克Transform
的同步由它来负责。
5.9、炮弹脚本:Projectile.cs
炮弹也是一个网络对象,它的行为脚本也必须继承NetworkBehaviour
,
// Projectile.cspublic class Projectile : NetworkBehaviour{}
炮弹预设实例化后,需要给Rigibody
一个力,从而让炮弹向前飞行,
// Projectile.csvoid Start(){rigidBody.AddForce(transform.forward * force);}
炮弹需要有一个生命周期控制,超过5秒
自动销毁,执行NetworkServer.Destroy(gameObject)
来销毁对象,
// Projectile.cspublic override void OnStartServer(){Invoke(nameof(DestroySelf), destroyAfter);}[Server]void DestroySelf(){NetworkServer.Destroy(gameObject);}
我们看到这里有一个[Server]
注解,它表示只有服务端可以调用此函数。
我们反编译可以看到它自动加了一个NetworkServer.active
判断,
我们再看[ServerCallback]
,它与[Server]
一样,只能在服务端调用,只是没有Warning
输出而已,如下
编译后:
完整的Projectile.cs
代码如下:
using UnityEngine;namespace Mirror.Examples.Tanks{public class Projectile : NetworkBehaviour{public float destroyAfter = 5;public Rigidbody rigidBody;public float force = 1000;public override void OnStartServer(){Invoke(nameof(DestroySelf), destroyAfter);}// set velocity for server and client. this way we don't have to sync the// position, because both the server and the client simulate it.void Start(){rigidBody.AddForce(transform.forward * force);}// destroy for everyone on the server[Server]void DestroySelf(){NetworkServer.Destroy(gameObject);}// ServerCallback because we don't want a warning if OnTriggerEnter is// called on the client[ServerCallback]void OnTriggerEnter(Collider co){NetworkServer.Destroy(gameObject);}}}
四、完毕
好了,就先写这么多吧~
最后再补充一个,我之前写了一篇关于UnityWebRequest
的文章,它也与网络通信相关,大家感兴趣的也可以看下:《长江后浪推前浪,UnityWebRequest替代WWW》
我是新发,喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,拜拜~
【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)