前言
Unity開發者對Profile並不會陌生,我們如何開發一個類似Profile的Editor工具來實現我們想要監控的數據呢,這裏以監控網絡消息包數據爲例,開發一個數據監控工具。
思路
主要就是採集數據和數據的表格化,採集數據我是以200毫秒時間內蒐集收發的數據列表做成一個數據包,表格繪製採用Handles.DrawAAPolyLine接口來繪製。
效果圖
代碼
#if UNITY_EDITOR
using BayatGames.SaveGamePro;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Base.Framework.Tools
{
public class MessageMonitorManager : RootMotion.Singleton<MessageMonitorManager>
{
private System.DateTime mTime = default(System.DateTime);
private MessageMonitorPacketGroup mCurretnMonitorPackageGroup;
private int mMillisecond = 200;//間隔200毫秒蒐集時間
Queue<MessageMonitorPacket> mMessages = new Queue<MessageMonitorPacket>();
Queue<MessageMonitorPacketGroup> mMessageGroup = new Queue<MessageMonitorPacketGroup>();
private bool mBeginCollectData = false;
//同步到本地的數據
List<MessageMonitorPacket> mSyncDatas = new List<MessageMonitorPacket>();
public bool CollectData
{
get
{
return mBeginCollectData;
}
set
{
if (!value)
{
SaveDataToLocal();
}
else
{
mMessageGroup.Clear();
mSyncDatas.Clear();
mMessages.Clear();
mMessageGroup.Clear();
mCurretnMonitorPackageGroup = null;
}
mBeginCollectData = value;
}
}
public UnityAction<MessageMonitorPacketGroup> MessageMonitorPackGroupEvent;
MessageMonitorPacket DequeMessage()
{
return mMessages.Dequeue();
}
public void EnqueueMessage(MessageMonitorPacket msg)
{
mMessages.Enqueue(msg);
mSyncDatas.Add(msg);
if (mTime == default(DateTime))
mTime = msg.MsgTime;
TimeSpan ts = msg.MsgTime - mTime;
if (ts.Milliseconds < mMillisecond)
{
CollectMessageMonitorPackGroup(msg);
}
else
{
EnqueueMessageGroup();
mTime = msg.MsgTime;
mCurretnMonitorPackageGroup = null;
CollectMessageMonitorPackGroup(msg);
}
}
void CollectMessageMonitorPackGroup(MessageMonitorPacket msg)
{
if (mCurretnMonitorPackageGroup != null)
{
mCurretnMonitorPackageGroup.Msgs.Add(msg);
mCurretnMonitorPackageGroup.MsgLength += msg.MsgLength;
}
else
{
mCurretnMonitorPackageGroup = new MessageMonitorPacketGroup();
mCurretnMonitorPackageGroup.MsgBeginTime = msg.MsgTime;
mCurretnMonitorPackageGroup.Msgs.Add(msg);
mCurretnMonitorPackageGroup.MsgLength += msg.MsgLength;
}
}
void EnqueueMessageGroup()
{
mMessageGroup.Enqueue(mCurretnMonitorPackageGroup);
if (EditorPlayMode._currentState == PlayModeState.Playing)
{
var msg = DequeueMessageGroup();
var floatValue = (float)(msg.MsgLength / 1024f);
msg.MsgKBLength = (float)Math.Round((double)floatValue, 1);
MessageMonitorPackGroupEvent?.Invoke(msg);
}
}
MessageMonitorPacketGroup DequeueMessageGroup()
{
return mMessageGroup.Dequeue();
}
void SaveDataToLocal()
{
if (mSyncDatas.Count > 0)
{
List<MessageMonitorPacket> datas = new List<MessageMonitorPacket>();
datas.AddRange(mSyncDatas);
SaveGame.SaveAsync<List<MessageMonitorPacket>>("NetMsgMonitorDatas.dat", datas);
}
}
}
}
#endif
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Base.Framework.Tools;
using System.Linq;
using System.Reflection;
public class DataAnalyzerTool : EditorWindow
{
static EditorWindow mWindow;
[UnityEditor.MenuItem("Tools/DataAnalyzer")]
private static void Open()
{
mWindow = EditorWindow.GetWindow(typeof(DataAnalyzerTool), true, "數據監視器", true);
mWindow.Show();
mWindow.Focus();
mWindow.position = new Rect(300, 50, 1190, 860);
}
const int LAYERS = 2;
//繪製參數
private GUIStyle mHeadStyle;
private Material mGraphMaterial;
private Vector2 scrollPos;
private Rect mAxisRect = new Rect(150, 50, 800, 300);
private Rect mGraphRect = new Rect(170, 70, 760, 280);
private Rect mGraphContentRect = new Rect(170, 70, 760, 280);
private Color[] mLayerColors = new Color[LAYERS]
{
new Color(190f / 255f, 192f / 255f, 40f / 255f),
new Color(54f / 255f, 137f / 255f, 168f / 255f),
};
private bool mClickGraph;
private int mCurrent;
private const int mSampleCount = 100;
private List<MessageMonitorPacketGroup> mSamples = new List<MessageMonitorPacketGroup>();
private Vector3[][] mPoints = new Vector3[LAYERS][];
PropertyInfo[] mProperties;
List<int> mWidths = new List<int>();
private void OnEnable()
{
mProperties = typeof(MessageMonitorPacket).GetProperties();
MessageMonitorManager.sInstance.CollectData = true;
InitDefaultData();
MessageMonitorManager.sInstance.MessageMonitorPackGroupEvent += OnMessageMonitorPackGroupCollected;
}
private void OnDisable()
{
MessageMonitorManager.sInstance.CollectData = false;
MessageMonitorManager.sInstance.MessageMonitorPackGroupEvent -= OnMessageMonitorPackGroupCollected;
}
private void OnMessageMonitorPackGroupCollected(MessageMonitorPacketGroup msg)
{
mSamples.RemoveAt(0);
mSamples.Add(msg);
}
private void OnInspectorUpdate()
{
if (EditorPlayMode._currentState == PlayModeState.Playing)
mWindow?.Repaint();
}
private MessageMonitorPacketGroup DefaultMessagePacket()
{
var msg = new MessageMonitorPacketGroup();
msg.MsgLength = 0;
msg.MsgKBLength = 0f;
msg.MsgBeginTime = System.DateTime.Now;
msg.Msgs = new List<MessageMonitorPacket>();
return msg;
}
private void InitDefaultData()
{
mSamples.Clear();
for (int i = 0; i < mSampleCount; i++)
{
mSamples.Add(DefaultMessagePacket());
}
}
private void OnGUI()
{
if (mHeadStyle == null)
{
mHeadStyle = new GUIStyle();
mHeadStyle.fontSize = 20;
mHeadStyle.alignment = TextAnchor.MiddleCenter;
mHeadStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
}
if (mGraphMaterial == null)
{
mGraphMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
mGraphMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
mGraphMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
mGraphMaterial.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off);
mGraphMaterial.SetInt("_ZWrite", 0);
}
if (EditorApplication.isPlaying)
{
if (mSamples.Count > 0)
DrawGraph();
HandleEvent();
}
//顯示數據列表
if (EditorPlayMode._currentState == PlayModeState.Paused && mCurrent != 0)
{
GUI.Label(new Rect(250, 450, 600, 40), "詳細數據", mHeadStyle);
GUILayout.Space(510);
DrawDetailInfos();
}
}
private void DrawDetailInfos()
{
mWidths.Clear();
EditorGUILayout.BeginHorizontal();
for (int i = 0; i < mProperties.Length; i++)
{
int w = (mProperties[i].Name.Length / 5 + 1) * 75;
mWidths.Add(w);
EditorGUILayout.LabelField(mProperties[i].Name, GUILayout.Width(w));
}
EditorGUILayout.EndHorizontal();
scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Width(1000), GUILayout.Height(350));
var msgs = mSamples[mCurrent]?.Msgs;
for (int i = 0; i < msgs.Count; i++)
{
EditorGUILayout.BeginHorizontal();
var data = msgs[i];
for (int j = 0; j < mProperties.Length; j++)
{
string showValue = mProperties[j].GetValue(data).ToString();
if (mProperties[j].Name.Contains("MsgId"))
{
showValue += $"({HotfixMessageIdList.MsgIdToType((ushort)mProperties[j].GetValue(data))})";
}
EditorGUILayout.LabelField(showValue, GUILayout.Width(mWidths[j]));
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
private void DrawGraph()
{
EditorGUI.LabelField(new Rect(mAxisRect.center.x - 400, mAxisRect.y - 50, 800, 50), "網絡消息數據監控", mHeadStyle);
if (mPoints[0] == null || mPoints[0].Length != mSampleCount)
{
for (int layer = 0; layer < LAYERS; ++layer)
mPoints[layer] = new Vector3[mSampleCount];
}
long maxValue = GetListMaxValue(mSamples);
for (int i = 0; i < mSamples.Count; ++i)
{
for (int layer = 0; layer < LAYERS; layer++)
{
mPoints[layer][i].x = (float)i / mSampleCount * mGraphContentRect.width + mGraphContentRect.xMin;
float showData = 0;
if (layer == 0)
{
showData = (float)mSamples[i].MsgKBLength;
}
else if (layer == 1)
{
showData = (float)mSamples[i].Msgs.Count;
}
mPoints[layer][i].y = mGraphContentRect.yMax - showData / maxValue * mGraphContentRect.height;
}
}
//畫邊(這裏的作用是去鋸齒)
Handles.BeginGUI();
for (int layer = 0; layer < LAYERS; ++layer)
{
Handles.color = mLayerColors[layer];
Handles.DrawAAPolyLine(mPoints[layer].Where(p => mGraphRect.Contains(p)).ToArray());
}
Handles.EndGUI();
//定位線
if (mGraphRect.Contains(mPoints[0][mCurrent]))
{
Handles.BeginGUI();
Handles.color = Color.white;
Handles.DrawAAPolyLine(3, new Vector2(mPoints[0][mCurrent].x, mAxisRect.yMin), new Vector2(mPoints[0][mCurrent].x, mAxisRect.yMax));
Handles.DrawAAPolyLine(2, new Vector2(mAxisRect.x, mPoints[0][mCurrent].y), mPoints[0][mCurrent]);
Handles.DrawAAPolyLine(2, new Vector2(mAxisRect.x, mPoints[1][mCurrent].y), mPoints[1][mCurrent]);
Handles.EndGUI();
EditorGUI.LabelField(new Rect(mPoints[0][mCurrent].x - 10, mAxisRect.yMax + 5, 50, 20), mCurrent.ToString());
EditorGUI.LabelField(new Rect(mAxisRect.xMin - 140, mPoints[0][mCurrent].y - 10, 200, 20), "PackageSize:" + mSamples[mCurrent].MsgKBLength.ToString() + " KB");
EditorGUI.LabelField(new Rect(mAxisRect.xMin - 140, mPoints[1][mCurrent].y - 10, 200, 20), "PackageCount:" + mSamples[mCurrent].Msgs.Count.ToString());
//詳細數據
string detail = string.Format($"MsgPackageCount:{mSamples[mCurrent].Msgs.Count},Length:{mSamples[mCurrent].MsgKBLength},Time:{mSamples[mCurrent].MsgBeginTime}");
EditorGUI.LabelField(new Rect(mAxisRect.center.x - 400, mAxisRect.yMax + 20, 800, 50), detail, mHeadStyle);
}
//座標軸
DrawArrow(new Vector2(mAxisRect.xMin, mAxisRect.yMax), new Vector2(mAxisRect.xMin, mAxisRect.yMin), Color.white);
DrawArrow(new Vector2(mAxisRect.xMin, mAxisRect.yMax), new Vector2(mAxisRect.xMax, mAxisRect.yMax), Color.white);
}
private void HandleEvent()
{
var point = Event.current.mousePosition;
switch (Event.current.type)
{
case EventType.MouseDrag:
{
if (Event.current.button == 0 && mClickGraph)
{
UpdateCurrentUI();
Repaint();
}
if (Event.current.button == 2 && mClickGraph)
{
mGraphContentRect.x += Event.current.delta.x;
if (mGraphContentRect.x > mGraphRect.x)
mGraphContentRect.x = mGraphRect.x;
if (mGraphContentRect.xMax < mGraphRect.xMax)
mGraphContentRect.x = mGraphRect.xMax - mGraphContentRect.width;
Repaint();
}
}
break;
case EventType.MouseDown:
{
mClickGraph = mGraphRect.Contains(point);
if (mClickGraph)
EditorGUI.FocusTextInControl(null);
if (Event.current.button == 0 && mClickGraph)
{
UpdateCurrentUI();
Repaint();
}
if (Event.current.button == 1)
{
Repaint();
}
EditorPlayMode.Pause();
}
break;
case EventType.KeyDown:
{
if (Event.current.keyCode == KeyCode.LeftArrow)
SetCurrentIndex(mCurrent - 1);
if (Event.current.keyCode == KeyCode.RightArrow)
SetCurrentIndex(mCurrent + 1);
Repaint();
}
break;
case EventType.ScrollWheel:
{
Repaint();
}
break;
}
}
private void UpdateCurrentUI()
{
float x = Event.current.mousePosition.x;
float distance = float.MaxValue;
int index = 0;
for (int i = 0; i < mPoints[0].Length; ++i)
{
if (mGraphRect.Contains(mPoints[0][i]) && Mathf.Abs(x - mPoints[0][i].x) < distance)
{
distance = Mathf.Abs(x - mPoints[0][i].x);
index = i;
}
}
SetCurrentIndex(index);
}
private void SetCurrentIndex(int i)
{
mCurrent = Mathf.Clamp(i, 0, mSampleCount - 1);
}
private string Color2String(Color color)
{
string c = "#";
c += ((int)(color.r * 255)).ToString("X2");
c += ((int)(color.g * 255)).ToString("X2");
c += ((int)(color.b * 255)).ToString("X2");
return c;
}
//繪製帶箭頭的線
private void DrawArrow(Vector2 from, Vector2 to, Color color)
{
Handles.BeginGUI();
Handles.color = color;
//繪製線
Handles.DrawAAPolyLine(3, from, to);
//箭頭
Vector2 v0 = from - to;
v0 *= 10 / v0.magnitude;
Vector2 v1 = new Vector2(v0.x * 0.866f - v0.y * 0.5f, v0.x * 0.5f + v0.y * 0.866f);
Vector2 v2 = new Vector2(v0.x * 0.866f + v0.y * 0.5f, v0.x * -0.5f + v0.y * 0.866f); ;
Handles.DrawAAPolyLine(3, to + v1, to, to + v2);
Handles.EndGUI();
}
private long GetListMaxValue(List<MessageMonitorPacketGroup> list)
{
List<long> newList = new List<long>();
for (int i = 0; i < list.Count; i++)
{
newList.Add(list[i].Msgs.Count);
}
return newList.Max();
}
}
填充實線
//填充曲線
_graphMaterial.SetPass(0);
for (int layer = 0; layer < LAYERS; ++layer)
{
GL.Begin(GL.TRIANGLE_STRIP);
GL.Color(_layerColor[layer]);
for (int i = 0; i < _samples.Count; ++i)
{
if (_graphRect.Contains(_points[layer][i]))
{
GL.Vertex(_points[layer][i]);
if (layer == LAYERS - 1)
GL.Vertex3(_points[layer][i].x, _graphContentRect.yMax, 0);
else
GL.Vertex(_points[layer + 1][i]);
}
}
GL.End();
}
在此思路的基礎上耐心擴展,還是可以做成類似Profile那麼強大的數據分析工具的,這裏只是提供的核心思路。