龙空技术网

「游戏开发实战」Unity使用Socket通信实现简单的多人聊天室

JAVA研究生 253

前言:

此时看官们对“unity和python代码如何连接”大致比较关注,咱们都想要分析一些“unity和python代码如何连接”的相关资讯。那么小编同时在网上搜集了一些对于“unity和python代码如何连接””的相关文章,希望各位老铁们能喜欢,兄弟们一起来了解一下吧!

文章目录二、简单的Socket通信:多人聊天室三、拓展:Mirror Networking1、局域网多人联机Demo的救星:Mirror4、Mirror 案例测试:多人坦克对战5、Mirror 案例讲解:多人坦克对战5.1、NetworkManager物体5.1.1、NetworkManager组件5.1.2、NetworkManagerHUD组件5.1.3、KcpTransport组件5.2、地面(带导航功能)5.2.2、导航烘焙:Navigation5.3、坦克生成点:NewworkStartPosition5.4.2、NavMeshAgent组件5.4.5、NetworkTransform组件5.4.6、NetworkIdentity组件5.4.7、NetworkBehaviour组件: Tank5.7、坦克脚本:Tank.cs5.8、Transform的网络同步:NetworkTransform.cs5.9、炮弹脚本:Projectile.cs一、前言

嗨,大家好,我是新发。

事情是这样的,上次有同学问我能不能出一期 网络 相关的教程,

然而我眼花看错了,看成了 网格 ,我还专门写了一篇文章: 《【游戏开发进阶】Unity网格探险之旅(Mesh | 动态合集 | 骨骼动画 | 蒙皮 )》

直到有同学在评论里提醒我,真是尴尬…

嘛,没事,今天就补上,写一篇 网络 相关文章。

我准备做个例子,使用 .Net 原生的 Socket 模块来实现简单的多人聊天室功能。

话不多说,我们开始吧~

二、简单的Socket通信:多人聊天室

Unity 中我们要实现网络通信,可以使用 .NetSocket 模块来实现。

为了演示,我就用 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)

关于 socketpython 函数原型可以使用 help(socket) 查看,

第一个参数是 socket domains (通信协议族),有两种类型: AF_UNIXAF_INET ,它们的区别:

通信协议族

说明

AF_UNIX

本机通信;另,它只能够用于单一的 Unix 系统进程间通信,不能在 Windows 系统中使用

AF_INET

TCP/IP 通信

第二个参数是 socket type (套接字类型),有 SOCKET_STREAMSOCK_DGRAMSOCK_RAW三种,

套接字类型

说明

SOCKET_STREAM

流式套接字,基于 TCP 通信,数据有保障(即能保证数据正确传送到对方),多用于资料(如文件)传送

SOCK_DGRAM

数据报套接字,基于 UDP 通信,数据是有保障的 , 主要用于在网络上发广播信息

SOCK_RAW

原始套接字,普通的套接字无法处理 ICMPIGMP 等网络报文,而 SOCK_RAW 可以; SOCK_RAW 也可以处理特殊的 IPv4 爆文;此外,利用原始套接字,可以通过IP_HDRINCL 套接字选项由用户构造IP头

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 完整代码如下:

'''作者:林新发,博客:功能:简单的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_server    g_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] = client    while 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、客户端:Unity2.1、创建工程,搭建场景

新建一个 Unity 工程,

使用 UGUI 简单搭建一下界面,如下

养成好习惯,界面保存为预设: TestPanel.prefab

2.2、Socket封装:ClientSocket.cs

我们先封装一个 ClientSocket.cs ,实现 Socket 的创建、连接和收发消息等功能。

2.2.1、构造Socket对象

// using System.Net.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封装 * 作者:林新发 博客:*/using System;using System.Net.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, 即16KB        m_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转string    string 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交互 * 作者:林新发 博客:*/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            {                 // 连接                var address = unameInput.text.Split(':');                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转string        string 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 ModeWindowed ,设置窗口默认宽高为 640 x 360

执行打包,

打包成功,

4、运行测试

先使用 python 运行服务端,

开启多个客户端,分别登录服务端,用户名分别是 皮皮猫林新发 吧~

服务端的输出:

开始聊天,

服务端的输出:

运行一切正常,完美。

5、工程源码

上面这个简单聊天室工程源码已上传到 CODE CHINA ,感兴趣的同学可自行下载下来进行学习,

工程地址:

注:我使用的 Unity 版本: Unity 2021.1.9f1c1 (64-bit)

另外关于 CODE CHINA 的使用教程我之前也写了一篇文章,感兴趣的同学可以看看:

《CODE.CHINA使用教程,创建项目仓库并上传代码(git)》

三、拓展:Mirror Networking1、局域网多人联机Demo的救星:Mirror

上面的简单聊天室功能,我们是做了一个独立的服务端负责消息的转发,聊天本身的逻辑非常简单,我们把大部分工作花在了维护 Socket 上,要解决多线程问题,要解决连接断开,要解决消息的序列化和反序列化等等。

有些同学做了一个单机版的小 Demo ,想改成局域网多人联机版,要处理好多复杂的同步问题,比如物理碰撞、状态同步等等,这个对于 Unity 萌新来说,不大友好。

有没有什么好用的网络库可以让开发更高效呢?有,那就是: Mirror

注:在 Unity 5.1 ~ Unity2018 中你可以使用 UNet (全称 Unity Networking ),到 Unity 2019 之后 UNet 就被废弃了, Mirror 就是来替代 UNet 的。你在网上搜到的 Unity Netwoking的教程就是 UNet ,它已经过时了,不要再使用 UNet 了!

2、关于Mirror

MirrorUnity 的高级网络 API ,支持不同的低级传输( UDPTCPKCP 等等)。

使用 Mirror ,客户端、服务端是在同一个工程中的,这就是为什么它叫 Mirror 。 也就是说它没有一个独立的服务端,而是由一台客户端作为 Host ,它既是客户端又是服务端,其他客户端连接这台 Host 客户端 。画成图是这样子:

Mirror 是开源的,它的社区很活跃,配套的文档也很详尽,大家可以从官网进行学习,不过是全英文的。

Mirror官网:

Mirror GitHub:

Mirror Asset Store:

Mirror 官方文档:

Mirror API手册:

Unity 与 Mirror的兼容:

Mirror 最适合 Unity 2019 LTS

Mirror 通常也适用于所有较新的 LTS 版本(即 2020 LTS )。

3、Mirror插件下载

建议从 Asset Store 上下载 Mirror 版本,因为 GitHub 的版本不一定稳定,

Asset Store 地址:

Mirror 插件添加到自己的账号中,然后回到 Unity ,在 Package Manager 中就可以下载了,

下载下来导入 Unity 中,

4、Mirror 案例测试:多人坦克对战

Mirror 中给我们提供了几个例子,

我以多人坦克对战为例,双击 Assets / Mirror / Examples / Tanks / Scenes/ Scene 进入场景,

运行后左上角出现三个按钮,如下

要开启两个客户端,为了方便演示,我先打出个 exe

打包成功后,运行两个客户端,其中一个作为 Host ,另一个客户端连接 Host ,运行效果如下:

可以看到我们对坦克的控制是实时同步到另一个端的。

5、Mirror 案例讲解:多人坦克对战

下面,我以多人坦克对战案例为例,给大家讲下制作过程。

5.1、NetworkManager物体

先创建一个空物体,重命名为 NetworkManager ,挂以下三个脚本:

NetworkManagerNetworkManagerHUDKcpTransport

5.1.1、NetworkManager组件

我们先看下官方手册:

意思就是, NetworkManager 是管理多个客户端连接的组件。它是多人联机游戏的核心控制组件。

一个场景中只能有一个激活的 NetworkManager (它是单例模式的)。

连接的服务端 IP 地址在 NetworkManager 中进行设置, Max Connections 是最大连接数。

(注意:任何一个客户端都可以同时是一个服务端)

5.1.2、NetworkManagerHUD组件

NetworkManagerHUD 组件是下面这个 GUI 的逻辑,通过它我们可以方便地进行测试。

5.1.3、KcpTransport组件

Mirror 帮我们封装了各种不同等级的传输协议(各种 Transport 组件),常用的是 KcpTransportTelepathyTransport

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组件

我们先看下官方手册:

意思就是说, NetworkTransform 组件会通过网络自动同步 positionrotationscale

NetworkTransform 组件的物体必须也带 NetworkIdentity 组件。

我们可以设置 PositonRotationScale 同步的敏感度,

为了让同步有一个平滑效果(不会一卡一卡的),我们可以勾选平滑差值,

5.4.6、NetworkIdentity组件

我们先看下官方手册:

意思就是说, NetworkIdentity 组件提供了游戏物体在网络中的唯一标识( ID )。

游戏运行过程中,我们在 Inspector 视图中预览到 NetworkIdentity 的信息。

5.4.7、NetworkBehaviour组件: Tank

Tank 脚本是坦克行为脚本,它继承 NetworkBehaviour

这里只讲 NetworkBehaviour 组件, Tank 具体代码后面再讲~

我们先看看官方手册:

意思就是说, NetworkBehaviour 脚本处理具有 NetworkIdentity 组件的游戏对象, NetworkBehaviour 的子类中可以处理高级 API 功能,例如 CommandsClientRpc'sSyncEventsSyncVars

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 。例子可以参见官方手册:

Mirror 提供的函数注解如下(部分注解我们上面已做了介绍),具体的注解可以参见 Mirror官方手册:

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 下载:

我们看到反编译出来的 TankCmdFire 函数的代码已经完全变了另外一个逻辑了,它发送了一个 “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 player            if (!isLocalPlayer) return;            // rotate            float horizontal = Input.GetAxis("Horizontal");            transform.Rotate(0, horizontal * rotationSpeed * Time.deltaTime, 0);            // move            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);            // shoot            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");        }    }}
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);        }    }}
四、完毕

原文

标签: #unity和python代码如何连接