引擎工具岗实习了接近一年,修改过引擎源码,也实现了各式各样需求,回顾起来还真是一长段路,感谢组内同事的帮助和网上各优秀博客(如Unity Editor 编辑器扩展功能相关Intel GPA安卓逆向相关)。于此就简略记录下引擎工具岗的基础内容,权当是离职总结回顾了,且行且看,尽力而为,不要害怕,不要后悔。

编辑器工具

  程序不仅有GamePlay向,也有Editor向,为策划美术提供工具使其能更好输出内容。在个人开发/小团队中可能不甚重要(或者根本没有),但如果是大型项目的话那就另当别论了。

编辑器分类

  大概可以分为两类,一类单开一个窗口,另一类在Inspector界面中显示

窗口编辑器

  窗口编辑器的主体结构如下:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEditor;

public class TestWindow : EditorWindow
{
static TestWindow window; //实例对象,必须静态
[MenuItem("Windows/TestWindow")] //定义菜单栏位置
static void Open() //打开窗口,必须是static
{
window = GetWindow<TestWindow>(false, "TestWindow", true); //实例化窗口
window.position = new Rect(600, 100, 450, 500); // (非必须)窗口位置和尺寸
window.Show(); //显示窗口
}
}

检视编辑器

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TestScript))] //指定所要绑定的Mono类型,即TestScript
public class TestScriptInspector : Editor
{
TestScript m_target;
private void OnEnable()
{
m_target = target as TestScript; //绑定target: The object being inspected
}

public override void OnInspectorGUI()
{
base.OnInspectorGUI(); //调用父类方法绘制TestScript原有可序列化数据.

if (GUILayout.Button("按钮"))//创建按钮
{
Debug.Log("按下按钮");//按钮事件
}
}
}

编辑器面板

  接下来是面板中如何组织各种结构,无可否认现在odin什么确实更整洁美观,但Unity自己的Editor更加易用易改。个人接触的工具均没有像Player Setting那般复杂的,所以更偏好使用Unity自带的Editor。

对象控制

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
EditorGUILayout.LabelField("标题", "内容");
foldOut = EditorGUILayout.Foldout(foldOut, "显示域");//折叠栏,类型为bool
if (foldOut)
{
toggle = EditorGUILayout.Toggle("开关", toggle);//类型为bool
intVal = EditorGUILayout.IntField("整型输入", intVal);//类型为int
stringVal = EditorGUILayout.TextField("文本输入", stringVal);//类型为string
vecVal = EditorGUILayout.Vector4Field("向量输入", vecVal);//类型为Vector4
layerVal = EditorGUILayout.LayerField("层级选择", layerVal);//类型为int
tagVal = EditorGUILayout.TagField("标签选择", tagVal);//类型为string
color = EditorGUILayout.ColorField("颜色选择", color);//类型为Color
curve = EditorGUILayout.CurveField("动画曲线", curve);//类型为AnimationCurve
}
foldOut = EditorGUILayout.Foldout(foldOut, "枚举对象");//复用折叠栏,主要不想给新变量取名
if (foldOut)
{
testEnum = (TestEnum)EditorGUILayout.EnumPopup("单选枚举", testEnum);//类型为自定枚举TestEnum
multEnum = (MultEnum)EditorGUILayout.EnumFlagsField("多选枚举", multEnum);//类型为自定枚举MultEnum
}
if (GUILayout.Button("按钮"))//创建按钮
{
Debug.Log("按下按钮");//按钮事件
}
reorderableList.DoLayoutList();//可拖动列表

其中的可拖动列表需要引入UnityEditorInternal(优化数组显示),并在绘制前对ReorderableList进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
reorderableList = new ReorderableList(listItems, typeof(string));//list类型为ReorderableList,item类型为List<string>
reorderableList.drawHeaderCallback = (Rect rect) =>
{
EditorGUI.LabelField(rect, "可拖动列表");
};
reorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
listItems[index] = EditorGUI.TextField(rect, listItems[index]);
};
reorderableList.onAddCallback = (ReorderableList list) =>
{
listItems.Add("New Item");
};
reorderableList.onRemoveCallback = (ReorderableList list) =>
{
listItems.RemoveAt(list.index);
};

界面排版

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
EditorGUILayout.BeginHorizontal();//横向布局
GUI.backgroundColor = Color.green;//绿色背景
if (GUILayout.Button("左按钮"))//创建按钮
{
Debug.Log("按下按钮");//按钮事件
}
EditorGUILayout.Space(10); //间隔10单位
GUI.backgroundColor = Color.blue;//蓝色背景
if (GUILayout.Button("右按钮"))//创建按钮
{
Debug.Log("按下按钮");//按钮事件
}
EditorGUILayout.EndHorizontal();//结束横向布局

消息提示

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
EditorGUILayout.HelpBox("一般提示,最常用的提示", MessageType.Info);
EditorGUILayout.HelpBox("警告提示,不会吧不会吧真有人管warning", MessageType.Warning);
EditorGUILayout.HelpBox("错误提示,噔噔咚恭喜是bug而非feature呢", MessageType.Error);
if(GUILayout.Button("我秦始皇打钱"))
{
string title = "信他还是信我秦始皇", text = "勇士,朕乃始皇帝,现被困于蜀道山中," +
"若勇士能解朕之困,待朕携百万秦甲,重御六合,朕当以勇士为相,赏金千两。";
if (EditorUtility.DisplayDialog(title, text, "义不容辞", "立刻付款"))//提示窗口
{
Debug.Log("与此同时几百公里外的蜀道山中,新建的信号基站被夜色染成了黑色,矗立在山顶," +
"星星在天空中俯瞰着这片大地,只不过这千百年前投下的目光,现在才来得及匆匆一瞥。");
}
else
{
Debug.Log("良久之后,寂朗的山中似乎传来了一声悠悠的叹息:“已经三千年过去了吗?" +
"原来朕竟沉睡了如此之久。”“那宛渠人果不负朕!”");
}
}

Odin编辑器

  既然前面提到过那就再稍微展示下,毕竟Odin是卖插件的,单独开了Attributes Example Window展示它的各种特性,感觉也不用我多做赘述了。

img

可视化编辑器

  这个倒是在辞职后做毕设时遇到的对话编辑器的需求,但当时毕竟自己单人开发,最后为了效率选择用CSV硬写。之后才开始了解相关,做了个对话编辑器并将可视化编辑器的内容并入此文。目前比较有名的开源节点编辑器有XNode和NodeGraphProcessor,个人比较喜欢XNode的界面,最终选择使用其来实现对话编辑器。下图是阶段性成果,内容选取的是《胆怯色上发光的画布》中的一段。

img

  XNode中分为Graph(整个可视化的图),Node(自定义的节点)和Port(节点间连线的端口)。为创建自定义的可视化编辑器,需要Graph脚本(创建对应的图),Node脚本(定义节点的内容),NodeEditor脚本(非必须,定义节点中的元素在图中如何绘制)。可以完全接入Odin并交由其控制,也可以使用Unity进行序列化并调用GUI进行绘制,个人选择的是后者(没钱买odin.jpg)。

编辑器文件操作

  这里有点需要注意,对直接Load下的Prefab文件进行修改的话是无法保存回去的,必须实例化后再进行修改保存。

1
2
3
4
5
6
7
8
9
10
AssetDatabase.CopyAsset(OldPath, NewPath) //复制文件
AssetDatabase.LoadAssetAtPath<Type>(Path) //加载文件
PrefabUtility.SaveAsPrefabAsset(File, Path); //保存文件

//基本读取,修改,保存操作
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefab/Cube.prefab"); //读取
GameObject obj = Instantiate(prefab); //实例化
obj.AddComponent<TestScript>(); //修改
PrefabUtility.SaveAsPrefabAsset(obj, "Assets/Prefab/Cube.prefab"); //保存
Destroy(obj); //销毁实例

截帧,性能分析与逆向

截帧

1. Unity自带的Frame Debug

  方便快捷,可以查看渲染顺序,渲染所用shader及纹理相关信息,可助快速定位问题

image-20231218174023418

2. RenderDoc

  可以查看顶点数据,shader代码并修改预览效果,遇到棘手渲染问题时可以一试

img

性能分析

  可以通过Unity自带的Profiler分析Unity各项函数开销,也可以通过代码开启记录以了解新功能模块的性能情况(更进一步可以试试Profile Analyzer,之前没有相关需要就没深入了解)

image-20231218181247676

逆向

  个人习惯在电脑上开模拟器使用Intel GPA逆向,首先开启Graphics Monitor的Auto-detect Launched Applications,之后运行游戏,在需要的场景截帧,最后在Graphics Frame Analyzer加载相关截图即可得到模型资源信息。

v2-7b3596110dca547486dd28838e38d401_720w v2-1bc7225b13177b86784ecb3379a3e14b_720w v2-411c3e75909a18c7f110ff3079338b2f_r