已经记不清这是福州的第几天高温预警了,感觉自己在非洲,春困,秋乏,夏打盹,工作还是不能落下=_=Zzz...
言归正传
咳咳咳,来看看今天和大家要分享的一个东西:网络收包显示工具。
在项目开发过程中,每一个游戏的需求是千变万化的,游戏种类也大相径庭,有单机,联网,有单人,多人。Unity虽然为我们提供了各种很方便的工具,但是要解决如此多的需求,还是有些力不从心,所以它开放了强大的编辑器扩展功能。用它的编辑器扩展,每一个游戏开发人员可以很方便的以Unity为平台开发自己游戏的工具链。
网络游戏开发过程中离不开和服务端的数据交互。之前我们项目服务端使用了Mina架构,游戏开发的匆忙(不要问我为什么匆忙,游戏公司的都懂/(ㄒoㄒ)/~~),所有服务端下发的数据都在Console控制台进行打印(你没有看错,直接打印...),控制台还会打印很多不同的信息,客户端与服务端联调的时候就十分费劲,需要不断的寻找下发的数据,心累,还是写个工具吧。
最终效果
知识点
1.Unity Editor使用
2.TreeView
3.LitJson 解析Json
动手撸代码
1.创建一个Editor窗体
新建名为NetListenerWindow的cs文件 ,放入Editor文件夹中(编辑器使用的代码,统一放Editor命名的文件夹,方便管理),使用MenuItem属性在编辑器中创建菜单入口。这样就初始化弹出一个和Unity自带的Scene和Game一样的Window窗体,之后左右的内容都需要显示在里面。
public class NetListenerWindow : EditorWindow
{
[MenuItem("Window/NetListener--消息包监听")]
static void Init()
{
NetListenerWindow window = (NetListenerWindow)EditorWindow.GetWindow(typeof(NetListenerWindow), true, "网络监听", true);
window.Show();
}
}
2.窗体布局
新弹出的窗体一片空白什么也没有,为了操作和显示的方便,需要对他进行简单的布局处理。Unity编辑器中布局使用的Unity最早的一套GUI系统 Immediate Mode GUI (IMGUI),布局主要使用GUILayout和GUIStyle进行纯代码控制,如果使用Unity较早的同学应该很熟悉这套东西,新上手的也没关系,可以先看看我的例子,文章末尾的扩展阅读也会有相关的资料。
编辑器的UI绘制需要写到OnGUI方法中,常用的方法有GUI控件的显示,GUI布局处理,GUI样式调整等,下面的代码是当前这个例子的GUI显示代码,包含详细的注解,欢迎食用~
void OnGUI()
{
if (GUILayout.Button("清空数据", EditorStyles.toolbarButton, GUILayout.Width(position.width)))
{
//绘制名为“清空数据”的按钮控件
//按钮的样式使用内置的EditorStyles.toolbarButton样式,也可以自己new GUIStyle进行设置
//GUILayout.Width方法设置按钮控件的绝对宽度为当前窗口的宽度
}
//创建一个横向流动布局,在一行中显示两个按钮
GUILayout.BeginHorizontal();
if (GUILayout.Button("展开全部", GUILayout.Width(position.width/2)))
{
//绘制名为“展开全部”的按钮控件
//GUILayout.Width方法设置按钮控件的绝对宽度为当前窗口宽度的1/2
}
if (GUILayout.Button("折叠全部",GUILayout.Width(position.width/2)))
{
//绘制名为“折叠全部”的按钮控件
//GUILayout.Width方法设置按钮控件的绝对宽度为当前窗口宽度的1/2
}
GUILayout.EndHorizontal();
//开始绘制一个滚动视图布局
GUILayout.BeginScrollView(Vector2.zero, GUILayout.Width(position.width), GUILayout.Height(position.height));
//绘制接收到的网络消息TreeView控件
GUILayout.EndScrollView();
}
界面效果:
3.定义接收网络包的展示TreeView控件
本来还担心自己要实现一个TreeView,查了下资料发现Unity已经有了这个功能(我使用的是Unity 5.6版),只需要在已有的控件类上进行简单的扩展就可以很方便的使用了。老规矩,带大量注释的代码。
//敲黑板,记得继承TreeView
class SimpleTreeView : TreeView
{
public TreeViewItem root;//TreeView的根节点
int itemIndex;//自增节点索引(每一个节点都需要一个id索引值,这里用自增型的)
//构造函数
public SimpleTreeView(TreeViewState treeViewState)
: base(treeViewState)
{
itemIndex = 1;
//TreeView里,depth代表节点的深度,最上层的根节点depth为-1
root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
//初始化的时候需要在根节点下插入一个子节点,不然会报Children null的错误
root.AddChild(new TreeViewItem { id = 1, depth = 0, displayName = "网络包接收" });
}
///
/// 获取自增的索引值
///
///
public int GetIndex()
{
itemIndex++;
return itemIndex;
}
//重载BuildRoot方法
protected override TreeViewItem BuildRoot()
{
SetupDepthsFromParentsAndChildren(root);
return root;
}
}
这里定义了一个简单的TreeView,TreeView中的每一个节点都用原生的TreeViewItem类实现,没有进行额外的扩展,当然你也可以根据实际的需要扩展这个类,用到自己的项目中。
4.定义&初始化
TreeViewState这个字段需要用[SerializeField]修饰,该字段是用来保存TreeView中的序列化后的信息(官方解释:The TreeViewState contains serializable state information for the TreeView)。netDatas用来保存接收到的网络消息,我这里是Json,各位具体项目中可以替换成自己对于的数据。
[SerializeField]
TreeViewState m_TreeViewState;
SimpleTreeView m_SimpleTreeView;
List netDatas;//保存接收到的网络消息
初始化数据的方法放到了OnEnable中,将m_TreeViewState传入我们定义的SimpleTreeView进行初始化。
void OnEnable()
{
if (m_TreeViewState == null)
m_TreeViewState = new TreeViewState();
m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
m_SimpleTreeView.Reload();
}
5.获取数据,生成TreeView
这一部分主要是Json数据的解析,并生成对应的TreeView。对于Editor工具,游戏代码无法访问到Editor,需要Editor不断的在游戏运行时去获取接收到的数据。这是就需要的客户端数据接收部分预留一个接口,获取对应的数据,然后在Editor中调用这个接口。在OnGUI中的方法做如下改动:
if (Mina.MinaClient.app != null)
{
GetNetDatas();
}
GUILayout.BeginScrollView(Vector2.zero, GUILayout.Width(position.width), GUILayout.Height(position.height));
m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
GUILayout.EndScrollView();
因为编辑器打开就在运行,而游戏只有在我们点击运行后才会执行,所以需要先判断游戏是否启动,这里我直接判断客户端Mina接收数据服务是否存在,如果存在就获取数据。
simpleTreeView的OnGUI需要在OnGUI中执行。
下面的代码是获取数据并生成TreeView的代码,每次GetNetDatas获取数据后,将缓存数据清除,就不会一直重复获取刷新,减少不必要的开销。
Json的解析包含的Json Object和Json Array两部分,有需要的朋友可以看看,不是使用Json的朋友可以略过。
生成TreeViewItem的时候要注意两点:
1.要注意depth值,depth表示了当前节点在树种的层级关系;
2.生成完一个节点的全部子节点,再生成下一个同级节点,因为TreeView的树结构是从上到下遍历生成的。
///
/// 获取网络包数据
///
void GetNetDatas()
{
if (Mina.MinaClient.netDatas.Count > 0)
{
netDatas = Mina.MinaClient.netDatas;
TreeViewItem item;
int eventid;//消息id
int retcode;//消息状态
JsonData content;//消息内容
int index;
for (int i = 0; i < netDatas.Count; i++)
{
eventid = (int)netDatas[i]["e"];
retcode = (int)netDatas[i]["r"];
if (netDatas[i].Keys.Contains("p"))
{
content = netDatas[i]["p"];
}
else
{
content = null;
}
index = m_SimpleTreeView.GetIndex();
item = new TreeViewItem { id = index, depth = 0, displayName = "eventid:" + eventid + " retcode:" + retcode };
m_SimpleTreeView.root.AddChild(item);
AddContentItem(item, content, 1);
}
m_SimpleTreeView.Reload();
netDatas.Clear();
Mina.MinaClient.netDatas.Clear();
}
}
///
/// 获取内容项目
///
///
///
void AddContentItem(TreeViewItem parentItem, JsonData content, int depth)
{
TreeViewItem item;
JsonData subContent;
int arrayIndex;
if (content == null)
return;
if (content.GetJsonType() == JsonType.Object)
{
foreach (var key in content.Keys)
{
subContent = content[key];
item = new TreeViewItem { id = m_SimpleTreeView.GetIndex(), depth = depth, displayName = key };
parentItem.AddChild(item);
AddContentItem(item, subContent, depth + 1);
}
}
else if (content.GetJsonType() == JsonType.Array)
{
arrayIndex = 0;
foreach (JsonData elem in content)
{
item = new TreeViewItem { id = m_SimpleTreeView.GetIndex(), depth = depth, displayName = arrayIndex.ToString() };
parentItem.AddChild(item);
arrayIndex++;
AddContentItem(item, elem, depth + 1);
}
}
else
{
item = new TreeViewItem { id = m_SimpleTreeView.GetIndex(), depth = depth, displayName = content.ToString() };
parentItem.AddChild(item);
}
}
这里还有个问题,如果大家只是做了这些会发现每次接收到数据的时候并不会实时刷新,因为当你运行游戏的时候,网络包接收窗口并不在Focus状态,OnGUI方法并没有激活执行,这时候你需要点击一下窗口才能收到数据,这显然不是我们想要的结果。这是我们就要在NetListenerWindow中加上下面这个方法,这个问题就完美解决啦。
void OnInspectorUpdate()
{
//这里开启窗口的重绘,不然窗口信息不会刷新
this.Repaint();
}
6.补齐按钮方法
按钮方法补齐下,OnGUI完整版
void OnGUI()
{
if (GUILayout.Button("清空数据", EditorStyles.toolbarButton, GUILayout.Width(position.width)))
{
m_SimpleTreeView.root.children.Clear();
m_SimpleTreeView.Reload();
}
GUILayout.BeginHorizontal();
if (GUILayout.Button("展开全部", GUILayout.Width(position.width/2)))
{
m_SimpleTreeView.ExpandAll();
}
if (GUILayout.Button("折叠全部",GUILayout.Width(position.width/2)))
{
m_SimpleTreeView.CollapseAll();
}
GUILayout.EndHorizontal();
if (Mina.MinaClient.app != null)
{
GetNetDatas();
}
GUILayout.BeginScrollView(Vector2.zero, GUILayout.Width(position.width), GUILayout.Height(position.height));
m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
GUILayout.EndScrollView();
}
可以改进的几个地方
上面这些代码已经可以实现一个基本的网络收包显示工具啦,在使用过程中发现还是有不少地方可以改进:
1.树结构需要展开和折叠的时候只能点击左边小小的箭头,这样操作不是很方便,需要支持双击展开、双击折叠功能;
2.网络包结构复杂的时候,一级一级进行展开不是很方便(虽然有清空、展开全部、折叠全部这些按钮辅助),可以改进成拆分显示的方式,左边显示树状结构,点击后右边直接把网络包用字符串打印出来(注意排版);
3.恩,样子有点丑,我需要个美术同学~~
这些改进是都可以实现的,具体怎么做吗,哎,看看有时间再写加强版咯
扩展阅读
Unity - Manual: TreeView
Unity - Scripting API: TreeView
Unity 折叠树 EditorGUI.Foldout
UnityEditor.IMGUI中的TreeView
Immediate Mode GUI (IMGUI) GUIScriptingGuide
结语
结语
大夏天写博客,随手点个赞咯,共同学习,共同进步,共勉之。