2015-05-08 00:22:04 yimingsilence 阅读数 986
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

(一)unity network初步

最新消息:在11月2日的开发者大会上,我问了一下unity的人,会不会对网络层进行封装和改进,unity的人表示,最晚5.0,最快4.x版本里,就会加入新的网络通信接口,他们已经在测试阶段了。 

    翻了半天unity的API,也没有发现可以处理Socket的方法。Unity自己封了个网络处理的类Network,主要用于状态同步。 似乎是要实现MMO级别的网络应用,只能用C#自己去写了。(Unity不像AS,有直接处理数据包、字节级别的底层函数。至少现在没有。如果您要应用于实际,比如做了个网游,那么您需要用unity自带的C#去写一套自定义通信协议来实现。这个我在后面的文章中会写一个示例。)

    我们还是先了解一下Unity,看看用Unity自带的组件,如何编写通信,从最简单的开始。


1,新建项目。新建一个项目里面会有一个摄像机。

2,编写服务器端代码cs:

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class server : MonoBehaviour {  
  5.       
  6.     int Port = 10000;  
  7.       
  8.     //OnGUI方法,所有GUI的绘制都需要在这个方法中实现  
  9.     void OnGUI(){  
  10.         //Network.peerType是端类型的状态:  
  11.         //即disconnected, connecting, server 或 client四种  
  12.         switch(Network.peerType){  
  13.             //禁止客户端连接运行, 服务器未初始化  
  14.             case NetworkPeerType.Disconnected:  
  15.                 StartServer();  
  16.                 break;  
  17.             //运行于服务器端  
  18.             case NetworkPeerType.Server:  
  19.                 OnServer();  
  20.                 break;  
  21.             //运行于客户端  
  22.             case NetworkPeerType.Client:  
  23.                 break;  
  24.             //正在尝试连接到服务器  
  25.             case NetworkPeerType.Connecting:  
  26.                 break;  
  27.         }  
  28.     }  
  29.       
  30.     void StartServer(){  
  31.         //当用户点击按钮的时候为true  
  32.         if (GUILayout.Button("创建服务器")) {  
  33.             //初始化本机服务器端口,第一个参数就是本机接收多少连接  
  34.             NetworkConnectionError error = Network.InitializeServer(12,Port,false);  
  35.             Debug.Log("错误日志"+error);  
  36.         }  
  37.     }  
  38.       
  39.     void OnServer(){  
  40.         GUILayout.Label("服务端已经运行,等待客户端连接");  
  41.         //Network.connections是所有连接的玩家, 数组[]  
  42.         //取客户端连接数.   
  43.         int length = Network.connections.Length;  
  44.         //按数组下标输出每个客户端的IP,Port  
  45.         for (int i=0; i<length; i++)  
  46.         {  
  47.             GUILayout.Label("客户端"+i);  
  48.             GUILayout.Label("客户端ip"+Network.connections[i].ipAddress);  
  49.             GUILayout.Label("客户端端口"+Network.connections[i].port);  
  50.         }  
  51.         //当用户点击按钮的时候为true  
  52.         if (GUILayout.Button("断开服务器")){  
  53.             Network.Disconnect();  
  54.         }  
  55.     }  
  56.       
  57.       
  58.     /* 系统提供的方法,该方法只执行一次 */  
  59.     // Use this for initialization  
  60.     void Start () {  
  61.       
  62.     }  
  63.       
  64.     // Update is called once per frame  
  65.     void Update () {  
  66.       
  67.     }  
  68. }  

3,把这个代码拖拽到摄像机上。

4,运行程序 File---->Build settings---->Build And Run,选择Web Player

5,服务器端建立完毕,保持服务器端的运行状态。

6,这里可以再新建立个项目写客户端。嫌麻烦也可以像我这样,先把摄像机上的服务器脚本删掉,再把下面的客户端脚本拖拽上去。

客户端代码如下:

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class client : MonoBehaviour {  
  5.     //要连接的服务器地址  
  6.     string IP = "127.0.0.1";  
  7.     //要连接的端口  
  8.     int Port = 10000;  
  9.           
  10.     void OnGUI(){  
  11.         //端类型的状态  
  12.         switch(Network.peerType){  
  13.             //禁止客户端连接运行, 服务器未初始化  
  14.             case NetworkPeerType.Disconnected:  
  15.                 StartConnect();  
  16.                 break;  
  17.             //运行于服务器端  
  18.             case NetworkPeerType.Server:  
  19.                 break;  
  20.             //运行于客户端  
  21.             case NetworkPeerType.Client:  
  22.                 break;  
  23.             //正在尝试连接到服务器  
  24.             case NetworkPeerType.Connecting:  
  25.                 break;  
  26.         }  
  27.     }  
  28.           
  29.           
  30.     void StartConnect(){  
  31.         if (GUILayout.Button("连接服务器")){  
  32.             NetworkConnectionError error = Network.Connect(IP,Port);  
  33.             Debug.Log("连接状态"+error);  
  34.         }  
  35.     }  
  36.           
  37.     // Use this for initialization  
  38.     void Start () {  
  39.       
  40.     }  
  41.       
  42.     // Update is called once per frame  
  43.     void Update () {  
  44.       
  45.     }  
  46. }  

7,运行程序 File---->Build settings---->Build And Run,选择Web Player

            (二)unity network聊天


在上一个例子基础上,我们构建一个聊天室程序。

1,首先建立一个新项目,文件夹名为chat。

2,给接收请求的脚本所绑定的对象,添加网络视图组件(听起来有点绕口)。我们的服务器脚本是绑定在主摄像机上的,所以点击主摄像机,在菜单上选择component-->miscellaneous-->Net work View。

3,服务器端程序:(C# 脚本)

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class server : MonoBehaviour {  
  5.       
  6.     int Port = 10100;  
  7.     string Message = "";  
  8.     //声明一个二维向量   
  9.     Vector2 Sc;   
  10.       
  11.     //OnGUI方法,所有GUI的绘制都需要在这个方法中实现  
  12.     void OnGUI(){  
  13.         //Network.peerType是端类型的状态:  
  14.         //即disconnected, connecting, server 或 client四种  
  15.         switch(Network.peerType){  
  16.             //禁止客户端连接运行, 服务器未初始化  
  17.             case NetworkPeerType.Disconnected:  
  18.                 StartServer();  
  19.                 break;  
  20.             //运行于服务器端  
  21.             case NetworkPeerType.Server:  
  22.                 OnServer();  
  23.                 break;  
  24.             //运行于客户端  
  25.             case NetworkPeerType.Client:  
  26.                 break;  
  27.             //正在尝试连接到服务器  
  28.             case NetworkPeerType.Connecting:  
  29.                 break;  
  30.         }  
  31.     }  
  32.       
  33.     void StartServer(){  
  34.         //当用户点击按钮的时候为true  
  35.         if (GUILayout.Button("创建服务器")) {  
  36.             //初始化本机服务器端口,第一个参数就是本机接收多少连接  
  37.             NetworkConnectionError error = Network.InitializeServer(12,Port,false);  
  38.             //连接状态  
  39.             switch(error){  
  40.                 case NetworkConnectionError.NoError:  
  41.                     break;  
  42.                 default:  
  43.                     Debug.Log("服务端错误"+error);  
  44.                     break;  
  45.             }  
  46.         }  
  47.     }  
  48.       
  49.     void OnServer(){  
  50.         GUILayout.Label("服务端已经运行,等待客户端连接");  
  51.         //Network.connections是所有连接的玩家, 数组[]  
  52.         //取客户端连接数.   
  53.         int length = Network.connections.Length;  
  54.         //按数组下标输出每个客户端的IP,Port  
  55.         for (int i=0; i<length; i++)  
  56.         {  
  57.             GUILayout.Label("客户端"+i);  
  58.             GUILayout.Label("客户端ip"+Network.connections[i].ipAddress);  
  59.             GUILayout.Label("客户端端口"+Network.connections[i].port);  
  60.             GUILayout.Label("-------------------------------");  
  61.         }  
  62.         //当用户点击按钮的时候为true  
  63.         if (GUILayout.Button("断开服务器")){  
  64.             Network.Disconnect();  
  65.         }  
  66.         //创建开始滚动视图  
  67.         Sc = GUILayout.BeginScrollView(Sc,GUILayout.Width(280),GUILayout.Height(400));  
  68.         //绘制纹理, 显示内容  
  69.         GUILayout.Box(Message);  
  70.         //结束滚动视图, 注意, 与开始滚动视图成对出现  
  71.         GUILayout.EndScrollView();    
  72.     }  
  73.       
  74.     //接收请求的方法. 注意要在上面添加[RPC]  
  75.     [RPC]  
  76.     void ReciveMessage(string msg, NetworkMessageInfo info){  
  77.         //刚从网络接收的数据的相关信息,会被保存到NetworkMessageInfo这个结构中  
  78.         Message = "发送端"+info.sender  +"消息"+msg;  
  79.         //+"时间"+info.timestamp +"网络视图"+info.networkView  
  80.     }  
  81.       
  82.       
  83.     // Use this for initialization  
  84.     void Start () {  
  85.       
  86.     }  
  87.       
  88.     // Update is called once per frame  
  89.     void Update () {  
  90.       
  91.     }  
  92. }  

4,把脚本拖拽到主摄像机上,绑定。

5,运行程序 File---->Build settings---->Build And Run,选择Web Player

6,服务器端建立完毕,保持服务器端的运行状态。

7,简单起见,我们这里不新建项目。先把摄像机上的服务器脚本删掉,再把下面的客户端脚本拖拽上去。

客户端代码:

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class client : MonoBehaviour {  
  5.     //要连接的服务器地址  
  6.     string IP = "127.0.0.1";  
  7.     //要连接的端口  
  8.     int Port = 10100;  
  9.     //聊天信息  
  10.     string Message = "";  
  11.     //声明一个二维向量   
  12.     Vector2 Sc;   
  13.           
  14.     void OnGUI(){  
  15.         //端类型的状态  
  16.         switch(Network.peerType){  
  17.             //禁止客户端连接运行, 服务器未初始化  
  18.             case NetworkPeerType.Disconnected:  
  19.                 StartConnect();  
  20.                 break;  
  21.             //运行于服务器端  
  22.             case NetworkPeerType.Server:  
  23.                 break;  
  24.             //运行于客户端  
  25.             case NetworkPeerType.Client:  
  26.                 OnClient();  
  27.                 break;  
  28.             //正在尝试连接到服务器  
  29.             case NetworkPeerType.Connecting:  
  30.                 break;  
  31.         }  
  32.     }  
  33.           
  34.     void StartConnect(){  
  35.         if (GUILayout.Button("连接服务器")){  
  36.             NetworkConnectionError error = Network.Connect(IP,Port);  
  37.             //连接状态  
  38.             switch(error){  
  39.                 case NetworkConnectionError.NoError:  
  40.                     break;  
  41.                 default:  
  42.                     Debug.Log("客户端错误"+error);  
  43.                     break;  
  44.             }  
  45.         }  
  46.     }  
  47.       
  48.     void OnClient(){  
  49.         //创建开始滚动视图  
  50.         Sc = GUILayout.BeginScrollView(Sc,GUILayout.Width(280),GUILayout.Height(400));  
  51.         //绘制纹理, 显示内容  
  52.         GUILayout.Box(Message);  
  53.         //文本框  
  54.         Message = GUILayout.TextArea(Message);  
  55.         if (GUILayout.Button("发送")){  
  56.             //发送给接收的函数, 模式为全部, 参数为信息  
  57.             networkView.RPC("ReciveMessage", RPCMode.All, Message);  
  58.         }  
  59.         //结束滚动视图, 注意, 与开始滚动视图成对出现  
  60.         GUILayout.EndScrollView();  
  61.           
  62.     }  
  63.       
  64.     //接收请求的方法. 注意要在上面添加[RPC]  
  65.     [RPC]  
  66.     void ReciveMessage(string msg, NetworkMessageInfo info){  
  67.         //刚从网络接收的数据的相关信息,会被保存到NetworkMessageInfo这个结构中  
  68.         Message = "发送端"+info.sender  +"消息"+msg;  
  69.     }  
  70.           
  71.     // Use this for initialization  
  72.     void Start () {  
  73.       
  74.     }  
  75.       
  76.     // Update is called once per frame  
  77.     void Update () {  
  78.       
  79.     }  
  80. }  

服务端运行状态:



客户端运行状态:


            (三)unity network物体同步

在上两篇的基础上,这次我们要做物体同步。使两个物体在两个机器上显示同样的移动效果。这里,使用W、S、A、D四个键位实现前后左右的移动。

注意:如果您复制粘贴代码,千万注意文件编码!最好为ANSI,否则会报出各种各样奇怪的错误!

 

步骤:

1,首先建立一个新工程。

2,添加两个cube,分别重命名为Cube01,和Cube02。这两个cube就是我们要同步的对象。

3,添加一个平行光源,Directional light。否则你在漆黑的场景中,真的很难找到你的cube。

4,调整摄像机、cube、Directional light 的相对位置,使他们在视野内,可以看着它们运行。

5,create一个C#脚本cube_move,如下:

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class cube_move : MonoBehaviour {  
  5.       
  6.     string IP = "192.168.1.6";  
  7.     int Port = 10100;  
  8.       
  9.     int moveSpeed = 16;  
  10.       
  11.     GameObject cube01 = null;  
  12.     GameObject cube02 = null;  
  13.       
  14.     GameObject myself = null;  
  15.     GameObject another = null;  
  16.       
  17.     // Use this for initialization  
  18.     void Start () {  
  19.         cube01 = GameObject.Find("Cube01");  
  20.         cube02 = GameObject.Find("Cube02");  
  21.     }  
  22.       
  23.     //OnGUI方法,所有GUI的绘制都需要在这个方法中实现  
  24.     void OnGUI(){  
  25.         //Network.peerType是端类型的状态:  
  26.         //即disconnected, connecting, server 或 client四种  
  27.         switch(Network.peerType){  
  28.             //禁止客户端连接运行, 服务器未初始化  
  29.             case NetworkPeerType.Disconnected:  
  30.                 StartServer();  
  31.                 break;  
  32.             //运行于服务器端  
  33.             case NetworkPeerType.Server:  
  34.                 OnServer();  
  35.                 break;  
  36.             //运行于客户端  
  37.             case NetworkPeerType.Client:  
  38.                 break;  
  39.             //正在尝试连接到服务器  
  40.             case NetworkPeerType.Connecting:  
  41.                 break;  
  42.         }  
  43.     }  
  44.       
  45.     void StartServer(){  
  46.         GUILayout.Label("同步测试:");  
  47.         //当用户点击按钮的时候为true  
  48.         if (GUILayout.Button("创建服务器")) {  
  49.             //初始化本机服务器端口,第一个参数就是本机接收多少连接  
  50.             NetworkConnectionError error = Network.InitializeServer(12,Port,false);  
  51.             //连接状态  
  52.             switch(error){  
  53.                 case NetworkConnectionError.NoError:  
  54.                     break;  
  55.                 default:  
  56.                     Debug.Log("服务端错误"+error);  
  57.                     break;  
  58.             }  
  59.         }  
  60.         if (GUILayout.Button("连接服务器")){  
  61.             NetworkConnectionError error = Network.Connect(IP,Port);  
  62.             //连接状态  
  63.             switch(error){  
  64.                 case NetworkConnectionError.NoError:  
  65.                     break;  
  66.                 default:  
  67.                     Debug.Log("客户端错误"+error);  
  68.                     break;  
  69.             }  
  70.         }  
  71.     }  
  72.       
  73.     void OnServer(){  
  74.         GUILayout.Label("服务端已经运行,等待客户端连接");  
  75.         //Network.connections是所有连接的玩家, 数组[]  
  76.         //取客户端连接数.   
  77.         int length = Network.connections.Length;  
  78.         //按数组下标输出每个客户端的IP,Port  
  79.         for (int i=0; i<length; i++)  
  80.         {  
  81.             GUILayout.Label("客户端"+i);  
  82.             GUILayout.Label("客户端ip"+Network.connections[i].ipAddress);  
  83.             GUILayout.Label("客户端端口"+Network.connections[i].port);  
  84.             GUILayout.Label("-------------------------------");  
  85.         }  
  86.         //当用户点击按钮的时候为true  
  87.         if (GUILayout.Button("断开服务器")){  
  88.             Network.Disconnect();  
  89.         }  
  90.     }  
  91.       
  92.     //接收请求的方法. 注意要在上面添加[RPC]  
  93.     [RPC]  
  94.     void ProcessMove(string msg, NetworkMessageInfo info){  
  95.         //刚从网络接收的数据的相关信息,会被保存到NetworkMessageInfo这个结构中  
  96.         string sender = info.sender.ToString();  
  97.         //看脚本运行在什么状态下  
  98.         NetworkPeerType status = Network.peerType;  
  99.         if (status == NetworkPeerType.Server)   
  100.         {  
  101.             myself = cube01;  //假如运行在server状态下, 那么自己就是cube1  
  102.             another = cube02;  
  103.               
  104.         }  
  105.         else  
  106.         {  
  107.             myself = cube02;  //假如运行在client状态下, 那么自己就是cube2  
  108.             another = cube01;  
  109.         }  
  110.         //假如是自己发送的信息  
  111.         if (sender == "-1")  
  112.         {  
  113.             if (msg == "W")  
  114.             {  
  115.                 myself.transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed);  
  116.             }  
  117.             if (msg == "S")  
  118.             {  
  119.                 myself.transform.Translate(Vector3.back * Time.deltaTime * moveSpeed);  
  120.             }  
  121.             if (msg == "A")  
  122.             {  
  123.                 myself.transform.Translate(Vector3.left * Time.deltaTime * moveSpeed);  
  124.             }  
  125.             if (msg == "D")  
  126.             {  
  127.                 myself.transform.Translate(Vector3.right * Time.deltaTime * moveSpeed);  
  128.             }  
  129.               
  130.         }  
  131.         //假如是别人发送的信息  
  132.         else  
  133.         {  
  134.             if (msg == "W")  
  135.             {  
  136.                 another.transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed);  
  137.             }  
  138.             if (msg == "S")  
  139.             {  
  140.                 another.transform.Translate(Vector3.back * Time.deltaTime * moveSpeed);  
  141.             }  
  142.             if (msg == "A")  
  143.             {  
  144.                 another.transform.Translate(Vector3.left * Time.deltaTime * moveSpeed);  
  145.             }  
  146.             if (msg == "D")  
  147.             {  
  148.                 another.transform.Translate(Vector3.right * Time.deltaTime * moveSpeed);  
  149.             }  
  150.         }  
  151.     }  
  152.     // Update is called once per frame  
  153.     void Update ()   
  154.     {  
  155.         //前移  
  156.         if (Input.GetKeyDown(KeyCode.W))  
  157.         {  
  158.             Debug.Log("wwwwwwwww|");  
  159.             networkView.RPC("ProcessMove", RPCMode.All, "W");  
  160.         }  
  161.         if (Input.GetKey(KeyCode.W))  
  162.         {  
  163.             networkView.RPC("ProcessMove", RPCMode.All, "W");  
  164.         }  
  165.         //后退  
  166.         if (Input.GetKeyDown(KeyCode.S))  
  167.         {  
  168.             Debug.Log("sssssssss!");  
  169.             networkView.RPC("ProcessMove", RPCMode.All, "S");  
  170.         }  
  171.         if (Input.GetKey(KeyCode.S))  
  172.         {  
  173.             networkView.RPC("ProcessMove", RPCMode.All, "S");  
  174.         }  
  175.         //向左移动  
  176.         if (Input.GetKeyDown(KeyCode.A))  
  177.         {  
  178.             networkView.RPC("ProcessMove", RPCMode.All, "A");  
  179.         }  
  180.         if (Input.GetKey(KeyCode.A))  
  181.         {  
  182.             networkView.RPC("ProcessMove", RPCMode.All, "A");  
  183.         }  
  184.         //向右移动  
  185.         if (Input.GetKeyDown(KeyCode.D))  
  186.         {  
  187.             networkView.RPC("ProcessMove", RPCMode.All, "D");  
  188.         }  
  189.         if (Input.GetKey(KeyCode.D))  
  190.         {  
  191.             networkView.RPC("ProcessMove", RPCMode.All, "D");  
  192.         }  
  193.     }  
  194. }  

6,给接收请求的脚本所绑定的对象,添加网络视图组件(听起来有点绕口)。我们的这个脚本是绑定在主摄像机上的,所以点击主摄像机,在菜单上选择component-->miscellaneous-->Net work View。

7,绑定脚本,把脚本拖拽到摄像机上。

8,编译发布成web的:File---->Build settings---->Build And Run,选择Web Player

9,拷贝编译出来的文件夹,到另一台机器上。

10,分别在两台机器上运行服务器端和客户端。

 

你会看到同步的效果了。

附注:在一台机器上,不太容易看到效果。因为浏览器失焦后,似乎无法刷新画面,导致同步效果不明显。

Ps. 这个代码只是unity自带的网络组件示例。在实际应用中,不能直接应用。需要用unity底层支持的C#语言,重新写一套通信框架。我已经写了一套,但是没有时间发博客。改天发出来。



2018-07-03 20:00:09 aawoe 阅读数 8595
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

Unity框架设计

将Unity Api、.NetFramework Api(4.6)以及部分原生库和托管库封装到一个抽象层,游戏本身的业务仅依赖于该抽象层从而提高业务逻辑的独立性和可维护性。 框架部分提供项目中使用的基础设施,包括资源管理、网络通信、UI框架、消息管理、场景管理、数据解析及存取等。

1.   资源管理

资源管理模块负责按照划分场景的颗粒度将所有游戏资源均打包至AssetBundle并在游戏中动态更新与加载,打包前需要将相资源索引文件和二进制资源(AB)放在StreamingAssetsPath路径下,在游戏初次运行时将所有资源拷贝至可读写路径PersistentDataPath下,在游戏更新阶段从服务器下载更新配置文件并根据本地资源的MD5更新资源文件,场景载入阶段异步加载场景需要的AssetBundle,资源加载时根据资源索引文件加载资源,离开场景时卸载相关AssetBundle,下图是流程示意图。

除了使用AssetBundle,Unity还支持使用Resources的动态加载资源方案,对比之下使用AssetBundle的主要优势为:可以较好控制游戏资源所占内存;可以从外部可读写路径加载所以支持热更新;可以实现边下边玩减小初始安装包体大小。AssetBundle的劣势为:资源之间的依赖关系难以处理,容易造成资源冗余(自己在处理这部分时遇到很多问题,处理不当场景中引用的资源也会和AssetBundle资源重复); 需要额外的编辑器扩展支持增加复杂度。考虑到该项目为联网游戏所以选择使用AssetBundle进行游戏资源管理。

资源管理策略:

资源索引文件是一个对称加密的哈希表,作为配置文件存放在本地的某个可读写目录,内容为由资源名和资源所在AssetBundle名构成的键值对。资源管理模块初始化时会加载该资源索引文件并在整个游戏的生命周期中保持资源名和AssetBundle名的映射,资源管理模块也会一直保持已加载AssetBundle镜像的引用,但是这些对业务层均是不可见的。

考虑到AssetBundle的占用内存较大,且资源之间引用比较复杂,默认情况下,在加载AssetBundle的同时也加载其内所有资源,且均使用异步加载。 卸载AssetBundle时会一并卸载其对应的所有资源。

考虑到通用资源的使用频率较高,资源管理模块提供一种策略,在加载通用AssetBundle时可以选择同时加载其内的所有资源并在整个游戏生命周期保留其引用,之后便卸载AssetBundle镜像,这样在没有增大游戏使用内存的前提下加快这部分资源的访问速度。

该方案的缺点是不同AssetBudle中的资源不允许重名,因而带来的优势为加载资源时只需提供资源名和资源类型(因为资源名和AssetBundle名存在一对一映射关系)从而隐藏了AssetBundle的复杂性。

ResourceManager

负责资源管理模块初始化,和服务器比对资源配置文件更新本地AB,更新资源索引文件,异步加载AssetBundle,卸载AssetBundle,加载游戏资源等功能。

AssetBundleHelper

作为Editor扩展, 负责生成本地资源索引文件和资源配置文件,上传资源文件至服务器。

AssetFileInfo

记录资源文件的基本信息,包括资源名、MD5值、下载路径、文件夹路径、资源大小等。

 

不足和反思:

a.    当前框架的更新是不可配置的,也就是将各种url,文件夹路径全都写死在了代码里,这在很大程度上限制了其通用性和扩展性,理想情况下最好将各种资源的配置以文件的形式存储,然后资源管理模块通过读取该文件进行初始化。

b.    这种方案要求不同AssetBundle中不允许重名资源,这一点如果在多人协作的时候很不容易保证,就算只有一个人管理也会不小心触发(Unity资源目录不同文件夹下的资源允许重名), 所以需要一个统一的资源命名规范,甚至需要一个工具来检查。

c.    有部分资源没有纳入资源管理模块的体系,如果场景引用了某个图集中的图片,那么在打包时会在本地拷贝整个图集会造成很大的资源冗余,所以只能选择拷贝一份重复的图片在场景中加以引用,但是如果这样同样会造成一部分资源冗余,同时这部分资源也无法热更新,目前仍没找到较好的解决方案。

d.    AssetBundleHelper的Editor扩展部分比较坑,需要运行游戏才能进行上传资源文件等联网操作(因为调用了网络模块),并且该操作也是不可取消的,这一部分有较大优化空间,可以单独写一个客户端来提供这部分功能。

e.    本地化问题,当项目做大的时候本地化是一个比较棘手的问题,当前的资源管理框架完全没有考虑本地化相关问题,该模块应当提供一种友好的方式来区分不同语言文化的资源并控制它们的加载。

f.     最大的不足是没有引入脚本资源的热更新,因为当时考虑到学习成本和开发成本,没有在框架中加入这部分内容,但是对于联网游戏来说代码的热更新十分重要,比较成熟的热更框架据说有uLua,xlua,ILRuntime,后续还需要继续学习。

 

2.   网络通信

网络通信模块负责向业务层提供短连接服务(Http请求)和Tcp长连接服务。

Http请求:

网络通信模块的Http请求使用了System.Net.Http.dll库,这个托管库是微软对HttpWebRequest封装的一层Http请求接口,网络通信模块在此之上进行封装实现异步Get/Post请求、下载文件等静态方法。

当然Unity本身也提供了很多Http请求的实现方式,例如WWW, UnityWebRequest均可以进行Http请求,但这两者都是基于协程实现的并发而不是并行,网络请求操作仍然是在主线程中执行。该项目使用Unity2017(.Net 4.6)开发,对多线程的支持已经比较完备,同时支持基于任务的异步编程模型,考虑将网络和I/O放在线程池中执行可以减小主线程压力,同时充分利用多核设备性能提高执行效率,所以设计网络通信模块时没有选用WWW和UnityWebRequest而是使用System.Net.Http.HttpClient。

HttpRequestClient:

实现并重载Http请求的多种静态异步方法,包括GetAsync(Get请求)、PostAsync(Post请求)、DownloadFileAsync(下载文件请求)。

HttpResponseData:

Http请求返回的数据格式(和后端约定),包括错误码、数据、消息等。

Tcp客户端:

网络通信模块基于System.Net.Sockets.TcpClient封装了异步阻塞模式的Tcp客户端(抽象基类BaseTcpClient),并提供了Tcp客户端常用方法的默认实现,包括连接、关闭连接、自动心跳、发送消息、包序排列、粘包分包处理、断线重连、断线回调、接收消息回调等功能。子类构造方法中需要提供IP地址/域名和端口即可创建一个Tcp客户端实例,一般情况下子类应当重写断线回调方法和接收消息回调方法,同时定义了默认的通信数据类型(前后端通信数据协议默认使用json)以支持Tcp客户端实现。

每个Tcp客户端实例内部会维护两个线程,一个线程用于定时发送自定义心跳包,一个线程用于阻塞等待接收消息包。Tcp客户端基类没有继承自MonoBehaviour,这样设计的初衷是希望让Tcp客户端在于游戏流程之外作为一个更封闭更独立的模块存在,Tcp客户端的生命周期管理方式也和Unity GameObject的生命周期不同,所以没必要继承MonoBehaviour。在实例化并连接成功之后,Tcp客户端的功能就是推动游戏流程,业务层应当订阅Tcp客户端中感兴趣的事件,当Tcp客户端收到消息包时便会把该消息包的处理方法派发到主线程消息队列,在主线程中执行订阅该事件的方法(因为绝大多数Unity Api只能在主线程中调用)  因为没有继承MonoBehaviour所以设计心跳包功能时没有选择在游戏周期的使用协程定时发送,而是单独开一个线程。

在处理粘包分包问题上,网络通信模块使用自定义包格式(包头+包体)的方式来处理。前后端通信的每个包在发送之前都会在原有数据之前添加4个字节的包头表示包体大小,客户端通过计算包头来获取每个包体,不完整的包则临时存储在缓冲区。

BaseTcpClient:

Tcp客户端基类,实现了常用方法,包括ConnectAsync(异步连接)、Close(关闭连接)、Reconnect(端线重连)、Send(发送消息)、OnReceived(接收消息回调)、OnLoseConnect(断线回调)等。

TcpRecvData:

默认实现的接收消息包的数据类型(和后端约定),包括包序、类型、数据(object,业务层解析)、错误码、消息。

TcpSendData:

默认实现的发送消息包的数据类型(和后端约定),包括用户ID、类型、数据(object,业务层解析)。

 

不足和反思:

a.    下载文件需要断点续传,这部分功能还需要进行优化,尤其是在更新资源包大小较大的时候,如果出现网络波动导致下载的进度丢失会给玩家造成较差的游戏体验。

b.    很多大佬都在建议使用Protobuf进行数据的结构化存储,但是该框架整体的数据存储和通信均使用json,Protobuf有更优的性能,占用更小的内存,所以将整个框架中的json数据存储方案替换为Protobuf存储方案会提高一些性能。

c.    目前仍没有实现UDP客户端,当前的网络通信模块没有实现一个UDP客户端基类,不排除项目之后有使用UDP协议通信的可能。

d.    异步方法的取消,整个框架中的大部分异步方法都是不支持取消的,尤其是网络和I/O部分有大量的异步方法,对于玩家来讲等待时间过久而又无法取消的体验是极不友好的,参照网上的方案可以在一些类型中实现接口以支持取消操作。

e.    关于自动发送心跳包和业务层发送消息引发的线程同步问题,这个问题目前还不是很清晰,因为业务层发送消息在主线程,自动发送心跳包在线程池,二者有可能同时执行发送消息的操作,而且网络流Netw orkStream好像不是线程安全的(不确定),在实践中后端反应过出过问题,但是把所有心跳操作都派发到主线程执行又违背了设计的初衷(封闭和独立),降低了这部分模块的扩展性(没法拿到别处用)。

 

3.   UI框架

UI框架基于UGUI简单封装了UI窗体的各种属性和方法,同时提供UI管理器作为业务层调用的接口。

把游戏中所有的2D可视元素均可以看作为窗体或窗体的一部分,例如一个设置界面、一个消息确认界面、一条吐司通知都可以抽象为一个窗体Form。但是在Unity中设计这样一个界面需要很多游戏物体组合,所以为了方便管理,将该窗体本身的显示元素均置于一个空的游戏物体StaticLayout。然后还要引入父窗体、子窗体、兄弟窗体的概念,因为考虑一个窗体在交互时会很可能产生隶属于该窗体的其它窗体,如果生成的窗体的生命周期应当小于等于原窗体的生命周期,那么它们就有逻辑上的父子关系,此时生成的窗体是原窗体的子窗体,原窗体是生成窗体的父窗体,拥有同一父窗体的窗体互为兄弟窗体,单独拿出每一个子窗体同样是一个独立的窗体整体。为了方便管理子窗体,所有子窗体都根据显示类型置于StackNode或NormalNode下。这三个节点(StaticLayout, StakcNode, NormalNode)统一挂在同一游戏物体下,并将Form组件添加到该游戏物体上,此时这个游戏物体在逻辑上就相当于一个窗体,将该游戏物体做成预制体资源,可以方便的在Inspector面板配置该窗体的属性(如显示类型, 预制名称 预制路径)和特性(如支持拖动, 模态显示, 固定显示, 全局唯一, 等),而窗体名称则需要在实例化该窗体时指定,下图是窗体层次结构示意图:

根据该层次结构也引入一个概念“虚拟层级路径”,是一个字符串,通过拼接根窗体和该窗体之间所有层次的窗体名称来作为唯一标识该窗体的索引。窗体在创建的时候有两种显示模式:堆栈和普通,任何时候堆栈模式的所有子窗体里只有一个子窗体是具有焦点的(可操作),而除此之外的其它子窗体均处在冻结状态,堆栈模式的子窗体任何时候只有栈顶的窗体具有焦点,当点击任一窗体或创建了新的子窗体时对应窗体移至栈顶。而普通模式的子窗体均是可操作的。

UIBaseForm:

是所有自定义窗体的基类,实现了一些窗体的特性和方法并提供了默认实现,子类可以重写InitContent(初始化窗体)、Show(显示窗体)、Freezed(冻结窗体)、Destroy(销毁窗体)、Recovery(从冻结状态恢复)等方法以便在UI窗体状态改变时更新其界面。

UIManager:

UI管理器,其内部有多个数据结构维护所有存在窗体的父子/兄弟关系,对外公开了CreateForm(创建窗体)、CloseForm(销毁窗体)等方法。

 

不足和反思:

a.    层级路径允许使用一个字符串来直接获取某个窗体的引用,虽然这样设计可以为获取一个窗体的引用提供了更灵活的方式。但是实际上父窗体直接拥有其所有子窗体的引用和控制权限而不必通过虚拟层级路径来获取,做同样一件事却有超过一种的方法,这在一定程度上违背了设计原则。本来这个UI框架中是没有虚拟层级路径的,但是个人能力有限,在实际开发的过程中发现很多时候业务层不易获取到UI窗体的引用,所以引入了虚拟层级路径这个概念,算一定意义的委曲求全。

b.    界面的显示逻辑和交互逻辑(也就是View层和Controller层)没有分开,因为毕竟当前该UI框架只是进行较简单的封装,继承BaseUIForm的自定义窗体类中,既有处理交互的代码,也有处理显示的代码,处理输入的部分和处理显示的部分耦合性较强。重新屡了下代码觉得这部分可以做如下优化:

l  任何一个继承自BaseUIForm的自定义窗体类都需要再实现一个对应的FormController类,并且均作为组件挂在窗体顶层上。以便取消窗体类的任何主动性,仅接收交互逻辑和业务逻辑的通知来更新UI,这样窗体类更轻了仅仅负责显示的功能,其处理交互部分的逻辑均在FormController类中实现。

c.    关于UI窗体的复用。复用重复创建的UI窗体可以提高性能, 虽然目前的UI框架支持使用对象池,但是和对象池系统整合得比较松散,理想情况下业务层对于创建UI窗体的复用过程应该是不可感知的,但是目前每个自定义窗体都需要实现对象池定义的接口,并且创建UI窗体之前需要手动判断在对象池中是否存在可复用对象显得很繁琐,可以想办法将这部分整合到UI管理器内部。

d.    关于UI根节点。该UI框架有个硬伤就是一定要存在某个根节点,也就是所有UI窗体的祖宗节点一定要是这个根节点,甚至需要单独为根结点挂载特殊的脚本,在处理UI逻辑中存在这样一个特例引入了一定的复杂性,在切换场景之后也要重新初始化根节点,可以优化取消根节点,只需在游戏载入阶段初始化一次UI框架即可。

 

4.   消息管理

为了降低模块之间的耦合性,使用一套消息通信机制很有必要,消息管理模块基于观察者模式提供了订阅、注销、发送消息等功能,同时实现了一个线程安全的类型来维护派发至主线程的消息队列。

虽然Unity本身提供了消息机制,如SendMessage、BroadcastMessage等方法,但是经过了解这种方法局限性较大:首先这样发送消息严重依赖字符串而无法实现编译阶段的类型安全,它也可以调用私有方法破坏类型的封装性,并且只有继承MonoBehaviour的类型才可以调用,这些应该都是该机制内部使用反射而带来的问题。所以有必要自己实现一套消息管理机制。

消息管理模块的核心消息管理器(MessageCenter),这是一个泛型静态类型,类型参数是一类枚举用来划分不同模块的消息,不同的类型参数有各自的消息队列,维护各自内部的消息处理。比如为了传递系统类型消息,设计一个SystemMessage枚举,用来枚举所有系统消息。这样设计主要是考虑通过区分枚举类型将不同类型的消息根据模块区分开便于维护和扩展,不同模块维护不同的消息队列,添加模块只需增加新的枚举类型即可。

MessageCenter:

消息管理器,对外公开了AddListener(订阅)、RemoveListener(注销)、Sendmessage(发送消息给监听者)等方法提供了基础的消息通信机制。

MainThreadMessageHandler:

内部维护一个委托队列,用来缓存派发至主线程的待执行方法,在主线程的Update周期中轮询来依次执行这些方法;对外公开了RegisterAction(派发主线程消息)方法。

 

不足和反思:

a.    在实现消息管理器时其实有两种方案:单例或者静态类,这部分的取舍很纠结,我当时是认为单例的生命周期和初始化顺序不容易控制,但是静态类不能继承也不面向对象,看网上相关资料推荐使用单例方案的较多,关于这个类型的设计还有不足。

b.    关于消息类型的基类。为了图方便,目前消息管理模块所使用消息的基类为System.EventArgs,但是这个类型几乎没有提供任何功能,目前该框架需要在EventArgs的基础上封装一个消息类型的基类,提供配合消息管理器的相关功能。

c.    发现消息管理器内部维护的字段类型是字典Dictionary<TMessage,EventHandler>,当时应该忽略了不同类型参数维护的不同静态字段,改为Queue<EventHandler>或者直接改为EventHandler应该都可以。

d.    还有一个问题一直存在但是没处理,就是派发出的消息抛出异常时,函数调用栈比较长,如果在Unity编辑器双击错误会直接定位到消息管理器内部,只能查看异常内部的调用栈来找到出错代码费时费力。可以把消息管理器部分编译为动态链接库作为插件引入项目,这样报错时可以直接定位到抛出异常的代码。

 

5.   场景管理

场景管理模块主封装了场景管理器和一个处理场景的通用基类。

该模块主要是为了提供唯一的场景入口和场景出口(包括游戏入口和游戏出口),并在入口处进行资源的加载和业务逻辑的初始化,在出口处进行资源的释放和相关业务逻辑的处理。这样做有利于控制正确的游戏流程顺序,避免初始化逻辑的分散。

每个新场景都需要一个实例添加继承自Scene基类的组件,并重写场景入口和场景出口方法来进行场景管理,同时提供一个全局的App组件,统一在App类中实现游戏入口和游戏出口方法,场景管理的流程如下图所示。

Scene:

自定义场景类的基类,提供虚方法SceneEntrance(场景入口)和SceneExport(场景出口)配合场景管理器进行场景切换

SceneManager:

场景管理器,封装了UnityEngine.SceneManagement.SceneManager的部分方法,公开了SceneConvertAsync(异步切换场景)供业务层调用。

App:

存在于整个游戏生命周期的组件,在其GameEntrance(游戏入口)方法中进行全局框架和全局模块的初始化,在其OnApplicationQuit(游戏出口)方法中进行相关操作。也提供了部分游戏周期事件供外部没有继承自MonoBehaviour的类型订阅(如Pause, Update, FixUpdate),这些消息被SystemMessage所枚举。

 

不足和反思:

a.    场景切换的进度汇报显示的问题,目前只是进行简单的遮挡处理,当游戏场景较大时没有进度提醒会造成玩家误解。

b.    关于“异步加载”的问题。在该框架中,场景和资源的加载是使用Unity的协程异步加载的,而网络和I/O部分则是使用多线程实现异步请求的,二者在同一个方法中使用会导致只能等待其中一个的结果,这十分棘手。因为一个方法要么返回IEnumertor,要么返回Task/Task<TResult>/void,也就是await和yield return 不能同时存在于一个方法中,毕竟这两种实现异步的机制不同。这个缺陷在切换场景时尤为放大,因为有时会需要在场景切换时进行网络请求和文件读写,目前没有想到较好的解决方案,只能在业务层尽可能避免同时使用两者。

 

6.   数据解析与存取

该模块主要向业务层提供文件存取的方法(包括异步和同步),并提供字符串的加密和解密服务,数据的序列化和反序列化通过调用第三方库Newtonsoft.Json实现。

文件存取部分封装了System.IO命名空间下的文件流操作方法并以较友好的方式公开;加密和解密使用DES对称加密方案,封装了System.Security.Cryptography命名空间下的相关类型。业务层进行文件存储的流程一般是先将相关对象序列化为字符串,进行加密之后存储到本地可写路径;进行文件读取的流程是先从本地可读路径读取字节流转化为字符串,进行解密后再反序列化为相应对象供业务层使用。

FileHelper:

提供文件存取的相关功能并重载大量方法方便调用,包括Write(写)、Read(读)、WriteAsync(异步写)、ReadAsync(异步读)、SelectFile(选取文件)等方法。

SecurityFactory:

基于DES对称加密方案提供对称加密相关功能;基于MD5加密方案提供不可逆加密相关功能,包括Encrypt(加密)和Decrypt(解密)等方法。

 

不足和反思:

a.    对称加密的安全性仍没有得到保证。为了简化加密解密过程,在设计该模块时直接将密钥直接置于代码里了,由于托管语言的特性,游戏逻辑编译过后的托管库(Assembly-CSharp.dll)在不经过处理的情况下是可以反编译的,密钥很容易会被获取。考虑应该有如下几种解决方案:使用工具进行代码混淆提高破解难度;使用IL2CPP方案将托管语言编译为原生语言提高安全性;将密钥置于服务器在游戏运行时联网获取。


2014-12-22 09:14:08 u014735301 阅读数 1581
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

前言

最近在学习unity自身封装的网络功能,在游戏物体上挂上Network组件,使其成为网络中的一份子,就像这里所说的,Unity网络协议本身是一个比较高层的网络协议,他并不是独立存在的,而是与引擎的游戏对象结合在一起,所以在Unity中不存在单纯的“发送给服务器”或“发送给客户端的”网络消息,不同的游戏对象之间的通信是独立的,就好像每个游戏对象都有一个独立的通信管道。

在游戏中,对象经常发生变化,需要进行状态同步,状态同步的方向只能是isMine到非isMine。比如一个玩家角色对象hp属性,如果需要其他玩家能够看到,就可以使用状态同步来完成。当isMine的玩家角色的hp变化后,会自动更新这个属性给其他的主机上的同一个玩家角色对象。状态同步还支持保证和非保证模式。

要进行状态同步就需要RPC,在其他机子上对同一游戏物体进行一个远程函数调用,RPC一般用来通过网络通知一次性的游戏事件,RPC的传输方向非常自由,可以在一个主机向任何其他主机,包括自己,发送RPC调用。

在unity网络中用到的相关类和方法
RPC 可以传递的参数
int
float
string
NetworkPlayer
NetworkViewID
Vector3
Quaternion
BitStream可以传递的参数
void Serialize (bool value)
void Serialize (char value) - only one byte [0, 255]
void Serialize (short value)
void Serialize (int value)
void Serialize (float value, float maxDelta = 0.00001f);
void Serialize (Quaternion value, float maxDelta = 0.00001f);
void Serialize (Vector3 value, float maxDelta = 0.00001f);
void Serialize (NetworkPlayer value)
void Serialize (NetworkViewID viewID)
RPCMode:
Server - only server
Others - send to everyone except the sender
OthersBuffered - Send to everyone except sender and adds to the buffer
All - send to everyone
AllBuffered - Sends to everyone and adds to the buffer
Network:
Network.AllocateViewID - 自动生成一个可用的viewid
Network.CloseConnection - 关闭当前连接
Network.Connect - 连接到指定端口或者域名
Network.connections -当前网络的连接数
Network.connectionTesterIP - 测试连接者的IP连接到服务器
Network.connectionTesterPort - 测试连接者的端口到服务器 (Debug)
Network.Destroy - 销毁跟当前viewID有关的网络连接
Network.DestroyPlayerObjects - 销毁跟当前Viewid有关的玩家物体
Network.Disconnect - 关闭所有已经连接好的连接,关闭网络网络接口
Network.GetAveragePing - ping 指定连接的玩家
Network.GetLastPing - 获取最后ping通的玩家
Network.HavePublicAddress - 检查当前机器是否有一个公共IP地址
Network.incomingPassword - 设置服务器的连接密码
Network.InitializeSecurity - 设置服务器加密,自动加密处理
Network.InitializeServer - 初始化服务器
Network.Instantiate - 实例化一个已经存在的prefab
Network.isClient - 判断当前的运行是否为客户端
Network.isMessageQueueRunning - 是否取消RPC访问或者取消网络数据同步
Network.isServer - 判断当前的运行是否为服务器
Network.logLevel - 设置当前网络信息的日志信息等级(sys)
Network.macConnections - 设置服务器可以连接的最大数或者游戏玩家数目
如果设置成0表示只有已经一个连接存在,设置成-1表示最大的连接数与当前
连接数相等,如果是这样,如果玩家失去连接,则服务器的位置将会为玩家
继续保留下来
Network.mininumAllocatableViewIDs - 设置最小从服务器获取的viewid数量*
Network.natFacilitatorIP - 使用网络穿透的IP地址
Network.natFacilitatorPort - 设置网络穿透的端口号
Network.OnConnectedToServer - [client]当连接服务器成功时调用
Network.OnDisconnectedFromServer - [client&svr]当服务器断开连接时调用
Network.OnFailedToConnect - [client]当客户端连接失败时
Network.OnNetworkInstantiate - [client|svr]当网络实例化一个对象调用
Network.OnPlayerConnected - [svr]当有一个新的玩家连接到服务器时
Network.OnPlayerDisconnected - [svr]当有玩家离开游戏时
Network.OnSerializeNetworkView - [client|svr]通过network view同步变量*
[void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
Network.OnServerInitialized - [svr]当服务器初始化成功时
Network.peerType - 网络对等状态
Network.player - 得到当前的NetworkPlayer实例引用
Network.proxyIP - 代理服务器的IP地址
Network.proxyPassword - 代理服务器的密码
Network.proxyPort - 代理服务器的端口号
Network.RemoveRPCs - 从服务器那里移除所有跟当前玩家相关的RPC方法
Network.RemoveRPCsInGroup - 从服务器那里移除跟当前组有关RPC方法
Network.sendRate - 网络同步的发送频率
Network.SetLevelPrefix - 设置网络前辍,将应用到所有viewid
Network.SetReceivingEnabled - 设置指定玩家的分组信息是否可以接收
Network.SetSendingEnable - 设置指定玩家的分组信息是否可以发送
Network.TestConnection - 测试当前主机的连接状态*
Network.TestConnectionNAT - 强制测试当前主机的连接状态
Network.time - 得到当前网络的时间,单位秒
Network.useProxy - 使用代理服务器

NetworkView:
NetworkView.Find - 通过netview id来查找一个NetworkView
NetworkView.group - 当前NetworkView所在的组
NetworkView.isMine - 当前物体是否有NetworkView控制功能
NetworkView.observed - 当前NetworkView是否在观察中
NetworkView.owner - 当前NetworkView是归谁所拥有
NetworkView.RPC - 调用当前所有已经连接了服务器的RPC方法
NetworkView.SetScope - 设置NetworkView的关联NetworkPlayer信息
NetworkView.stateSynchronization - 网络数据同步发送安全性*
NetworkView.viewID - 当前NetworkView的NetworkViewID

NetworkViewID:
NetworkViewID.isMine - 是否是自己实例化的物体
NetworkViewID.operator!= - 如果两个NetworkView不相等
NetworkViewID.operator== - 如果两个NetworkView相等
NetworkViewID.owner(NetworkView) - 哪个NetworkPlayer拥有的NetworkViewID
NetworkViewID.ToString - 返回已经格式化好的NetworkViewID信息
NetworkViewID.unassigned - 表示当前networkview id无效

NetworkPlayer:
NetworkPlayer.externalIP - 获取当前NetworkPlayer对外开放的IP地址
NetworkPlayer.externalPort - 获取当前NetworkPlayer对开开放的端口号
NetworkPlayer.guid - 当前NetworkPlayer的GUID,当使用NAT punchthrough时
NetworkPlayer.ipAddress - 当前NetworkPlayer的IP地址
NetworkPlayer.port - 当前NetworkPlayer的端口号
NetworkPlayer.operator!= - 是否两个NetworkPlayer不相等
NetworkPlayer.operator== - 是否两个NetworkPlayer相等
NetworkPlayer.ToString - 返回当前NetworkPlayer的索引

HostData:
HostData.comment - 主机描述信息
HostData.connectedPlayers - 当前的玩家连接数
HostData.gameName - 当前主机对应的游戏名称
HostData.gameType - 当前主机对应的游戏类型
HostData.guid - NAT punchthrough时
HostData.ip - 主机的IP地址
HostData.passwordProtected - 主机是否有密码保护
HostData.playerLimit - 主机最大能承受的玩家数量
HostData.port - 当前主机的端口号
HostData.useNat - 当前主机是否已经有Nat穿透功能
MasterServer:
MasterServer.ClearHostList - 清除通过从MasterServer.PollHostList获取的主机列表
MasterServer.dedicatedServer - 当前机器是否有专有服务器
MasterServer.ipAddress - 主服务器的IP地址
MasterServer.OnFailedToConnectToMasterServer - [svr|client]当连接主服务器失败时回调
MasterServer.OnMasterServerEvent - 当主服务器有事件通知[MasterServerEvent]
MasterServer.PollHostList - 通过MasterServer.RequestHostList检查最新的主机列表
MasterServer.port - 主服务器的端口号
MasterServer.RegisterHost - 注册当前服务器到主服务器
MasterServer.RequestHostList - 人主服务器那里请求主机列表
MasterServer.UnregisterHost - 从主服务器那里取消注册当前服务器
MasterServer.updateRate - 设置主服务器的最小更新频率,如果为0,则主服务器不进行更新,
默认更新频率为60s


小练习
首先搭建一个场景,人物,在人物身上挂上Network,人物控制脚本等。
创建服务器(同时也是客户端),将脚本挂在主摄像机上
using UnityEngine;
using System.Collections;

public class AuthoritativeNetworkServer : MonoBehaviour {

	private string ip="127.0.0.1";
	private int port=10001;
	
void OnGUI(){
		
		
	switch(Network.peerType){
		
		case NetworkPeerType.Disconnected:
			StartCreat();
			
			break;
		case NetworkPeerType.Server:
			OnServer();
			
			break;
		case NetworkPeerType.Client:
			OnClient();
		
			break;
		case NetworkPeerType.Connecting:
			GUILayout.Label("连接中···");
			break;
		}
		
		
	}
	
	void StartCreat(){
		
			GUILayout.BeginVertical();
			if(GUILayout.Button("新建服务器")){
				
				NetworkConnectionError error=Network.InitializeServer(30,port);
				Debug.Log (error);
				
			}
			if(GUILayout.Button("连接服务器")){
				
				NetworkConnectionError error=Network.Connect(ip,port);
				Debug.Log(error);
				
			}
			GUILayout.EndVertical();
		
		
		}
	void OnServer(){
		GUILayout.Label(" 服务端创建成功,请等待连接");
        int length=Network.connections.Length;
		for(int i=0;i<length;i++){
			
			GUILayout.Label("客户端连接ip:"+Network.connections[i].ipAddress);
            GUILayout.Label("客户端连接端口:"+Network.connections[i].port);
			
		}
				
		if(GUILayout.Button("断开服务器")){ 
			Network.Disconnect();
				
		}
		
	}
	void OnClient(){
		
		GUILayout.Label("连接成功");
		if(GUILayout.Button("断开连接")){
		
			Network.Disconnect();
			
		}
		
		
	}
	
}
设置一个人物生成点,挂上 人物创建脚本,当创建服务器或者连接服务器成功时,生成一个游戏人物。可以在地面上通过鼠标点击进行行走,当客户端断开连接时,销毁对应的游戏对象。这里要注意的几点:
1 unity的网络机制是在联网时,在各个机器上创建游戏对象实例,所以要保证照相机发出的射线只能控制当前游戏对象,不能控制其他机器的游戏对象,我的做法是将当前player和network.player(本地实例)比较,相同说明就是当前角色,可以进行射线操作
	//服务端新建完成后运行
	void OnServerInitialized(){
		
		MovePlayer(Network.player);
		
	}
    //玩家连接后运行
	void OnPlayerConnected(NetworkPlayer player){
		
		MovePlayer(player);
       	
	}
	void MovePlayer(NetworkPlayer player){
	//获取玩家id值
	int playerID=int.Parse(player.ToString());
	//初始化目标对象
        Transform playerTrans=(Transform)Network.Instantiate(playerPrefab,transform.position, transform.rotation, playerID);	
        NetworkView playerObjNetworkview=playerTrans.networkView;
        //添加HeroController组件至集合中 用于断开连接时删除对象
        list.Add(playerTrans.GetComponent("HeroController"));
        //调用RPC,函数名为SetPlayer
	playerObjNetworkview.RPC("SetPlayer",RPCMode.AllBuffered,player);
	}
看看setPlayer方法写了什么:
    //调用客户端(这里包括服务端)的SetPlayer函数
    [RPC]
    void SetPlayer(NetworkPlayer player)
    {
         ownerPlayer = Network.player;
        if (player == Network.player)
        {
            //当前的客户端能对其操作,否则不能
            canDo = true;
        }
    }
2 角色的状态需要同步,比如人物的坐标,要用到onSerializeNetworkView方法,用来在一个由网络视图监控的脚本中自定义变量同步。它自动决定被序列化的变量是否应该发送或接收,取决于谁拥有该物体,即所有者发送,其他所有人接收。
    void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
    {
        Vector3 pos = Vector3.zero;
        if (stream.isWriting)
        {
            pos = this.transform.position;
            stream.Serialize(ref pos);
        }
        else
        {
            stream.Serialize(ref pos);
            this.transform.position = pos;
        }
    }
这样人物的位置信息就会传达到各个客户端了,但是动画等状态不能通过这个方法传递,仍需要调用RPC函数
	//玩家断开连接时调用
    void OnPlayerDisconnected(NetworkPlayer player) {
        //遍历集合对象上的PlayerControl组件
	    foreach(PlayerControl script in list){
		    if(player==script.ownerPlayer){
                //移除所有属于这个id的rpc函数
			    Network.RemoveRPCs(script.gameObject.networkView.viewID);
                //销毁物体
			    Network.Destroy(script.gameObject);
			    list.Remove(script);
			    break;
		    }
	    }
	    int playerNumber= int.Parse(player+"");
	    Network.RemoveRPCs(Network.player, playerNumber);
	    Network.RemoveRPCs(player);
	    Network.DestroyPlayerObjects(player);
    }

    //客户端断开连接时调用
    void OnDisconnectedFromServer(NetworkDisconnection info) {
	
	    Application.LoadLevel(Application.loadedLevel);	
    }

最后大概实现了,创建服务器,生成玩家,连接服务器,生成玩家,点击地面对玩家进行操作,玩家位置相互同步。





2020-02-21 00:09:17 ha_____ha 阅读数 141
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

目录

 

1.项目说明

2.Socket通信

2.1C#实现socket通信

2.2python实现socket通信

3.结果

3.1C#socket通信结果

3.2python和C# socket通信结果


1.项目说明

项目使用unity3D搭建环境,并通过python程序控制unity环境。在控制过程中,需要使用socket通信将python处理后的数据发送到unity环境中。由于第一次接触socket通信,因此,首先使用C#搭建了客户端和服务器端进行实验,然后使用python搭建服务器,C#搭建客户端进行实验,最后在unity上进行测试。由于unity和python程序都运行在同一台电脑上,我们使用IP地址为127.0.0.1。整个通信过程是,建立连接后,客户端向服务器端发送字符串“location”,服务器端接收到字符串后,将一个坐标发送到客户端。

2.Socket通信

Socket封装了TCP/IP协议,可以视为TCP/IP协议向程序员开发所提供的接口,从而简化了网络编程。Socket在网络通信中的位置如图所示。

2.1C#实现socket通信

我们使用的是异步socket通信,主要流程为建立socket连接,收发数据,关闭连接。

(1)引入命名空间

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

(2)建立socket对象和事件状态

由于异步通信,当socket进行通信时,主线程将会执行后面的内容。但后面的程序可能需要在 socket通信之后才能进行,因此需要事件状态来阻塞主线程的运行。

public Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        public static ManualResetEvent allDone = new ManualResetEvent(false);
        private static ManualResetEvent receiveDone =
        new ManualResetEvent(false);
        private static ManualResetEvent connectDone = new ManualResetEvent(false);
        private static ManualResetEvent sendDone =
        new ManualResetEvent(false);
        private static String response = String.Empty;
        static void Main(string[] args)

(3)建立socket连接

biginconnect对远程主机发送建立连接的异步请求,这里异步方法除了一般的参数外,还有两个参数,一个是回调函数,另一个是状态对象,状态对象向回调函数提供异步通信的状态,对应于回调函数中的参数IAsyncResult。IAsyncResult是接口,里面有几个属性用来查看可以查看异步操作的状态:AsyncState 获取用户定义的对象,它限定或包含关于异步操作的信息。回调函数主要对后续任务和通信的状态进行处理。回调函数中,应调用 EndConnect 方法。 当应用程序调用 BeginConnect 时,系统将使用单独的线程执行指定的回调方法,并在 EndConnect 上一直阻止到 Socket成功连接或引发异常为止。 此外,我们不希望在建立socket连接时,主线程执行其他任务,因此主线程中使用waitone进行阻塞。当需要原始线程继续执行时,在回调方法中调用ManualResetEvent 的 Set 方法。

string address = "127.0.0.1";
            IPAddress hostIP = IPAddress.Parse(address);
            int port = 50088;
 clientSocket.BeginConnect(hostIP, port, new AsyncCallback(connectCallback), clientSocket);
                connectDone.WaitOne();

private void connectCallback(IAsyncResult asyncConnect)
        {
            
            try
            {
                Socket client = (Socket)asyncConnect.AsyncState;
                client.EndConnect(asyncConnect);
                Console.WriteLine("Socket connected to {0}", client.RemoteEndPoint.ToString());
                // Signal that the connection has been made.     
                connectDone.Set();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
           
        }

(4)发送数据

在成功建立连接后,客户端向服务器发送字符串“location”,首先将字符串进行ASCII编码,并调用异步方法beginsend发送数据,该异步方法同样有回调函数,对发送状态进行处理,解除主线程的阻塞。

Send(clientSocket, content);
                sendDone.WaitOne();
 private static void Send(Socket handler, String data)
        {
            // Convert the string data to byte data using ASCII encoding.     
            byte[] byteData = Encoding.ASCII.GetBytes(data);
        
            // Begin sending the data to the remote device.     
            handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler);
        }
        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.     
                Socket handler = (Socket)ar.AsyncState;
                // Complete sending the data to the remote device.     
                int bytesSent = handler.EndSend(ar);
                Console.WriteLine("Sent {0} bytes to server.", bytesSent);
                sendDone.Set();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

(5)接收数据

服务器接收到“location”后,将坐标(12.3,13.6)发送给客户端。因此客户端开始接收数据。首先创建stateobject对象存放接收的数据对象,然后调用异步方法beginreceive。在回调函数中,由于传送的数据量可能大于buffer容量,因此需要多次读取,才能接收到全部的数据,因此使用if (state.sb.Length > 1)进行判断,如果满足条件,则再次调用beginrreceive方法。

 Receive(clientSocket);
                receiveDone.WaitOne();
private static void Receive(Socket client)
        {
            try
            {
                // Create the state object.     
                StateObject state = new StateObject();
                state.workSocket = client;
                // Begin receiving the data from the remote device.     
                Console.WriteLine("async receive location");
                client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
        private static void ReceiveCallback(IAsyncResult ar)
        {

            try
            {
                // Retrieve the state object and the client socket     
                // from the asynchronous state object.     
                
                StateObject state = (StateObject)ar.AsyncState;
                Socket client = state.workSocket;
                // Read data from the remote device.     
                int bytesRead = client.EndReceive(ar);
                
                if (bytesRead > 0)
                {
                    // There might be more data, so store the data received so far.     

                    state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
                    // Get the rest of the data.     
                    client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
                }
                else
                {
                    // All the data has arrived; put it in response.     
                    if (state.sb.Length > 1)
                    {
                        response = state.sb.ToString();
                        Console.WriteLine(response.ToString());
                    }
                    // Signal that all bytes have been received.     
                    receiveDone.Set();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

(6) 关闭套接字连接

项目中仅需要进行坐标的传递,在接收到坐标后即可关闭套接字连接。clientSocket.Shutdown通知服务器端或客户端停止接收和发送数据,参数SocketShutdown.Both意味着客服端和服务器端都停止。

clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();

(7)服务器端

以上操作都是针对客户端的,而服务器端收发数据的操作也大致相同,服务器端和客户端的套接字对应相同的IP和端口,其余主要的区别是套接字建立连接时的操作不同。服务器端需要将套接字和IP端口绑定。BeginAccept开始一个异步操作来接受一个传入的连接尝试。回调函数中,套接字连接成功后,从客户端接收数据,接收到“location”时,再将坐标发送过。

IPAddress local = IPAddress.Parse("127.0.0.1");
                IPEndPoint iep = new IPEndPoint(local, 50088);
                server.Bind(iep);
                server.Listen(100);
                while (true)
                {
                    allDone.Reset();
                    Console.WriteLine("Waiting for a connection...");
                    server.BeginAccept(new AsyncCallback(Acceptcallback), server);
                    allDone.WaitOne();
                   
                }
public static void Acceptcallback(IAsyncResult iar)
        {
            // Signal the main thread to continue.     
            
            //还原传入的原始套接字
            Socket listener = (Socket)iar.AsyncState;
            //在原始套接字上调用EndAccept方法,返回新的套接字
            Socket handler = listener.EndAccept(iar);

            // Create the state object.     
            StateObject state = new StateObject();
            state.workSocket = handler;
            handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
            receiveDone.WaitOne();
            allDone.Set();

            // Create the state object.     
            //StateObject state = new StateObject();
            //state.workSocket = handler;
            //handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
        }

(8)C#代码链接

https://github.com/wzyzyw/Socket

2.2python实现socket通信

python主要介绍关于服务器端的创建。

(1)创建socket对象并绑定IP端口

s=socket.socket()
s.bind(('127.0.0.1',50088))
s.listen()

(2)接收客户端的请求连接

accept()接受一个客户端的连接请求,并返回一个新的套接字,与客户端通信是通过这个新的套接字上发送和接收数据来完成的。

c, addr = s.accept()

 

(3)从套接字读取数据并发送数据

c.recv(1024).decode('ascii')

c.send(str_content.encode('ascii'))

(4)完整python代码

import socket

s=socket.socket()
s.bind(('127.0.0.1',50088))
s.listen()
sync_robot_loc=[12.3,13.6]
def def_socket_thread():
    # global loop
    # loop = asyncio.get_event_loop()

    try:
        while True:
            c, addr = s.accept()
            content = read_from_client(c)
            if content.find('location') > -1:
                global sync_robot_loc
                print('receive request')
                print('sycn position=', sync_robot_loc)
                x = sync_robot_loc[0]
                y = sync_robot_loc[1]
                str_content = 'x=' + str(x) + ',y=' + str(y)
                c.send(str_content.encode('ascii'))
                print('finish location send')
            else:
                print('no request')
    except IOError as e:
        print(e.strerror)
    print('start socket thread!!!')


def read_from_client(c):
    try:
        return c.recv(1024).decode('ascii')
    except IOError as e:
        # 如果异常的话可能就是会话中断 那么直接删除
        print(e.strerror)

if __name__=='__main__':
    def_socket_thread()

3.结果

3.1C#socket通信结果

consoleapp1是客户端显示的结果,consoleapp2是服务器端显示的结果,意味着成功实现了socket通信。

3.2python和C# socket通信结果

下图是python服务器端的结果

下图是C#客户端的结果

意味着成功实现了socket通信。

 

2017-10-13 19:56:01 qq_37551067 阅读数 1167
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

前言

    为了保证数据的安全性,在客户端服务器进行通信之前要对数据进行一次封装来保证数据的安全,在接受到数据后在进行拆包,就像是送快递之前要把里面的东西给包装好,在接到快点后在拆开.
    压缩工具可以在网上下载ZipHelper开源的压缩帮助类 数据流读取,CRC16,Xor异或加密类可以直接使用 封装和拆包为示例

封装数据包:

* 先判断是否采用压缩,定义了一个ushort的常量,判断数据的长度大于这个常量就进行压缩,否则就不压缩
* 封装的方法采用了先压缩在异或加密最后进行CRC校验
* 完整的数据包是 "包头"+"包体"=完整数据包
* 包头是ushort类型的数据,存放包体的长度
* 包体包括
    * Boll值--压缩的标志--1个字节
    * ushrot类型的--CRC校验码--2个字节
    * 加密后的数据

封装好的数据包格式

拆封数据包

(根据包头的包体长度拿到包体的数据之后进行拆封数据包)

* 封装数据包时是先进行的压缩再异或加密最后CRC校验,拆包时就是反过来的
* 把包体的数据接到一个byte的数组中
* 包体的长度减去三个字节的长度就是经过压缩并异或的数据长度,用一个数组存放真实数据
* 把包体的所有数据都写入流中
* 用一个变量接受压缩标志
* 用一个ushort变量接受crc校验码
* 最后数据流中剩余的就是真实的数据 ,用前面定义好的数组接收
* 接收到真实数据后计算一下接受到的数据的crc校验码,判断传过来的校验码和计算数据的校验码是否一致,不一致就停止操作数据
* crc一致就证明数据是正确的,直接进行异或解密
* 再通过传输过来的压缩标志来进行判断是否有进行过压缩操作,有压缩就进行解压
* 最后拿到了最真实的数据,就通过传输协议进行获取数据

封包

  /// <summary>
    /// 封装数据包
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    private byte[] MakeData(byte[] data)
    {
        byte[] retBuffer = null;
        //1.获取压缩标志
        bool isCompress = data.Length > m_CompressLen ? true : false;
        //2.判断是否需要压缩
        if (isCompress)
            data = ZlibHelper.CompressBytes(data);
        //3.获取真实数据异或加密后的数据
        data = SecurityUtil.Xor(data);
        //4.获取Crc冗余校验码(压缩&&异或)
        ushort crc = Crc16.CalculateCrc16(data);
        Debug.Log("客户端CRC="+crc);
        using (MMO_MemoryStream ms = new MMO_MemoryStream())
        {
            //5.包体长度写入数据流中
            ms.WriteUShort((ushort)(data.Length + 3));
            //6.压缩标志写入数据流中
            ms.WriteBool(isCompress);
            //7.Crc冗余校验码写入数据流中
            ms.WriteUShort(crc);
            //8.Xor加密后的数据写入流中
            ms.Write(data, 0, data.Length);
            //9.获取Make后的完整数据包
            retBuffer = ms.ToArray();
        }
        return retBuffer;
    }

拆包

//存放接受到的队列中的包体(压缩标志|crc|Xor异或&&压缩后的数据(真实数据))
                        byte[] buffer = m_ReceiveQueue.Dequeue();
                        //存放Xor异或&&压缩后的数据
                        byte[] bufferNew = new byte[buffer.Length - 3];
                        //压缩标志
                        bool isCompress = false;
                        //接受过来的Crc校验码
                        ushort crc = 0;
                        //获取实际数据包体和协议编号
                        using (MMO_MemoryStream ms = new MMO_MemoryStream(buffer))
                        {
                            //从数据流中读取压缩标志
                            isCompress = ms.ReadBool();
                            //从数据流中读取Crc校验码
                            crc = ms.ReadUShort();
                            //从数据流中读取Xor加密的数据
                            ms.Read(bufferNew, 0, bufferNew.Length);
                        }
                        //计算传输过来数据的Crc(压缩&&异或)
                        int newCrc = Crc16.CalculateCrc16(bufferNew);
                        //temp
                        Debug.Log("服务器NewCRC=" + newCrc);
                        if (newCrc == crc)
                        {
                            //异或得到原始数据
                            bufferNew = SecurityUtil.Xor(bufferNew);
                            if (isCompress)
                                //对数据进行解压缩
                                bufferNew = ZlibHelper.DeCompressBytes(bufferNew);
                            ushort protoCode = 0;
                            byte[] protoContent = new byte[bufferNew.Length - 2];
                            using (MMO_MemoryStream ms = new MMO_MemoryStream(bufferNew))
                            {
                                //读取协议编号
                                protoCode = ms.ReadUShort();
                                //读取最原始的数据
                                ms.Read(protoContent, 0, protoContent.Length);
                                //派发协议事件
                                EventDispatcher.Instance.Dispatch(protoCode, protoContent);
                            }
                        }
                        else
                        {
                            break;
                        }

Xor异或加密类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Socket Xor加密
/// </summary>
public sealed class SecurityUtil
{
    #region xorScale 异或因子
    /// <summary>
    /// 异或因子
    /// </summary>
    private static readonly byte[] xorScale = new byte[] { 45, 66, 38, 55, 23, 254, 9, 165, 90, 19, 41, 45, 201, 58, 55, 37, 254, 185, 165, 169, 19, 171 };//.data文件的xor加解密因子
    #endregion
    private SecurityUtil()
    {
    }
    /// <summary>
    /// 对数据进行异或
    /// </summary>
    /// <param name="buffer"></param>
    /// <returns></returns>
    public static byte[] Xor(byte[] buffer)
    {
        //xor加密
        int iScaleLen = xorScale.Length;
        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = (byte)(buffer[i] ^ xorScale[i % iScaleLen]);
        }
        return buffer;
    }
}

CRC冗余校验类

public class Crc16
{
    // Table of CRC values for high-order byte
    private static readonly byte[] _auchCRCHi = new byte[] { 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 };
    // Table of CRC values for low-order byte
    private static readonly byte[] _auchCRCLo = new byte[] { 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 };
    /// <summary>
    /// 获得CRC16效验码
    /// </summary>
    /// <param name="buffer"></param>
    /// <returns></returns>
    public static ushort CalculateCrc16(byte[] buffer)
    {
        byte crcHi = 0xff;  // high crc byte initialized
        byte crcLo = 0xff;  // low crc byte initialized
        for (int i = 0; i < buffer.Length; i++)
        {
            int crcIndex = crcHi ^ buffer[i];
            // calculate the crc lookup index
            crcHi = (byte)(crcLo ^ _auchCRCHi[crcIndex]);
            crcLo = _auchCRCLo[crcIndex];
        }
        return (ushort)(crcHi << 8 | crcLo);
    }
}

数据流读写类

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using System;
/// <summary>
/// 数据转换(Byte short int long float decimal bool string)
/// </summary>
public class MMO_MemoryStream : MemoryStream
{
    public MMO_MemoryStream()
    {
    }
    public MMO_MemoryStream(byte[] buffer)
        : base(buffer)
    {
    }
    #region Short
    /// <summary>
    /// 从流中读取一个short数据
    /// </summary>
    /// <returns></returns>
    public short ReadShort()
    {
        byte[] arr = new byte[2];
        base.Read(arr, 0, 2);
        return BitConverter.ToInt16(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个short数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteShort(short value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region UShort
    /// <summary>
    /// 从流中读取一个UShort数据
    /// </summary>
    /// <returns></returns>
    public ushort ReadUShort()
    {
        byte[] arr = new byte[2];
        base.Read(arr, 0, 2);
        return BitConverter.ToUInt16(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个uShort数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteUShort(ushort value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region Int
    /// <summary>
    /// 从流中读取一个int数据
    /// </summary>
    /// <returns></returns>
    public int ReadInt()
    {
        byte[] arr = new byte[4];
        base.Read(arr, 0, 4);
        return BitConverter.ToInt32(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个int数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteInt(int value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region UInit
    /// <summary>
    /// 从流中读取一个Uint数据
    /// </summary>
    /// <returns></returns>
    public uint ReadUInt()
    {
        byte[] arr = new byte[4];
        base.Read(arr, 0, 4);
        return BitConverter.ToUInt32(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个uint数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteUInit(uint value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region long
    /// <summary>
    /// 从流中读取一个long数据
    /// </summary>
    /// <returns></returns>
    public long ReadLong()
    {
        byte[] arr = new byte[8];
        base.Read(arr, 0, 8);
        return BitConverter.ToInt64(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个long数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteLong(long value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region ULong
    /// <summary>
    /// 从流中读取一个ULong数据
    /// </summary>
    /// <returns></returns>
    public ulong ReadULong()
    {
        byte[] arr = new byte[8];
        base.Read(arr, 0, 8);
        return BitConverter.ToUInt64(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个ULong数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteULong(ulong value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region Float
    /// <summary>
    /// 从流中读取一个float数据
    /// </summary>
    /// <returns></returns>
    public float ReadFloat()
    {
        byte[] arr = new byte[4];
        base.Read(arr, 0, 4);
        return BitConverter.ToSingle(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个float数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteFloat(float value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region Double
    /// <summary>
    /// 从流中读取一个Double数据
    /// </summary>
    /// <returns></returns>
    public double ReadDouble()
    {
        byte[] arr = new byte[8];
        base.Read(arr, 0, 8);
        return BitConverter.ToDouble(arr, 0);
    }
    /// <summary>
    /// 向流中写入一个double数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteDouble(double value)
    {
        byte[] arr = BitConverter.GetBytes(value);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
    #region Bool
    /// <summary>
    /// 从流中读取一个Bool数据
    /// </summary>
    /// <returns></returns>
    public bool ReadBool()
    {
        return base.ReadByte() == 1;
    }
    /// <summary>
    /// 向流中写入一个bool数组
    /// </summary>
    /// <param name="value"></param>
    public void WriteBool(bool value)
    {
        base.WriteByte((byte)(value == true ? 1 : 0));
    }
    #endregion
    #region UTF8String
    /// <summary>
    /// 从流中读取一个string数组
    /// </summary>
    /// <returns></returns>
    public string ReadUTF8String()
    {
        ushort len = this.ReadUShort();
        byte[] arr = new byte[len];
        base.Read(arr, 0, len);
        return Encoding.UTF8.GetString(arr);
    }
    /// <summary>
    /// 把一个字符串数字写入流
    /// </summary>
    /// <param name="str"></param>
    public void WriteUTF8String(string str)
    {
        if (string.IsNullOrEmpty(str)) return;
        byte[] arr = Encoding.UTF8.GetBytes(str);
        if (arr.Length > 65535)
        {
            throw new InvalidCastException("字符串超出范围");
        }
        this.WriteUShort((ushort)arr.Length);
        base.Write(arr, 0, arr.Length);
    }
    #endregion
}
没有更多推荐了,返回首页