在Unity中使用工具生成GameObject节点的FullPath

最终效果

  • GameObject上面右键生成配置信息

ImgGIF

  • 生成CS文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Code.AutoGenerate
{
public class GUIPath
{
public class Demo
{
public const string NameTF = "a0";
public const string InfoBox = "a1";
public const string Info01 = "a2";
public const string InfoBox_Info01_MsgTF = "a3";
public const string InfoBox_Info01_Icon = "a4";
public const string InfoBox_Info02 = "a5";
public const string InfoBox_Info02_MsgTF = "a6";
public const string InfoBox_Info03_MsgTF = "a7";
}

}
}
  • 运行时候通过生成的文件查找对应节点的FullPath
1
2
3
4
5
6
7
8
9
public class Test01 : MonoBehaviour
{
private void Start()
{
var go = transform.Find(GameGUI.FullPath(GUIPath.Demo.InfoBox_Info01_Icon));
var img = go.GetComponent<Image>();
img.color = Color.green;
}
}

问题

对于在项目中是否使用 public 属性然后在Inspector中进行赋值 的方式进行 GUI界面的制作.一直存在两个声音. 有人支持,有人反对.

各有各的道理吧,因为各自项目不同 所以也不好一概而论.

Private 赋值方式的优点

就我个人而言,目前来说 我还是比较倾向于用 private 声明然后再通过transform.Find()方式赋值的方法.

我认为这种方法有几个优点

  • 对混淆友好: 以Inspector进行拖拽赋值的方法 肯定是无法混淆的

  • 抗灾能力强: 尤其是对于研发周期在半年以上的中大型项目,且是多人同时提交代码. 当出现一两次Missing Reference的事故时候,你会更加深入理解这个问题的:)

  • 易于模块化开发: 对于复杂的面板,我一般会拆分多个组件,然后主GUI上挂载Manger类进行其内部组件的协调. 如果采用public赋值方法,所有组件的变量都需要在Manger类内部进行引用切赋值.导致这个类内部很乱.并且所有子组件都需要和Manger进行交叉引用.增大了组件之间的耦合度.

Private 赋值方式的缺点

缺点就我现在的体会来说 就是觉得赋值很麻烦.

Jietu20180724-170141

比如像上面这段,编写期间要不停的Unity和Rider之间切换然后Copy字符串. 并且如果出现结构上的重构时候.也很麻烦.

解决方案

我还是按照之前处理在Unity项目中隐藏StringConst方式一样.

写GUI工具,同时生成一份CS文件和一个二进制文件.

然后代码中直接通过CS文件中定义的Key去找二进制文件所放置的FullPath即可

核心代码

将工具入口放置在右键菜单中

1
[MenuItem("GameObject/UI/${你自定义的子目录}")]

详情参考Unity Editor Extensions – Menu Items

使用内部类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace Code.AutoGenerate
{
public class GUIPath
{
public class Demo
{
public const string NameTF = "a0";
public const string InfoBox = "a1";
public const string Info01 = "a2";
public const string InfoBox_Info01_MsgTF = "a3";
public const string InfoBox_Info01_Icon = "a4";
public const string InfoBox_Info02 = "a5";
public const string InfoBox_Info02_MsgTF = "a6";
public const string InfoBox_Info03_MsgTF = "a7";
}

public class Demo_2
{
public const string NameTF = "a8";
public const string InfoBox = "a9";
public const string Info01 = "a10";
public const string InfoBox_Info01_MsgTF = "a11";
public const string InfoBox_Info01_Icon = "a12";
public const string InfoBox_Info02 = "a13";
public const string InfoBox_Info02_MsgTF = "a14";
public const string InfoBox_Info03_MsgTF = "a15";
}
}
}

以需要构建的GameObject的Name作为内部类的类名去构建自动生成的CS文件. 这样在代码中会更加清晰.

Jietu20180724-185801

使用名称约束

Jietu20180724-190052

  • S_开头的节点会生成短名称,比如上图的InfoBox

  • F_开头的节点会生成长名称(用以区分同名节点).比如上图的InfoBox_Info01_MsgTF

  • 不以S_F_开头的节点则不导出.(代码中不会直接操作该节点)

遍历一个GameObject取得其FullPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

private GUIFullPathNode GetNewNode(GameObject _go)
{
var rootNode = new GUIFullPathNode();
rootNode.nodeList = new List<GUIFullPathGameObjectWrap>();
LoopCreateFullPath(_go.transform, _go.transform, rootNode);
return rootNode;
}


private void LoopCreateFullPath(Transform _currentTrans, Transform _rootTrans, GUIFullPathNode _rootNode)
{
var nodeName = _currentTrans.name;
if (nodeName.StartsWith(SHORT_NAME_PR) || nodeName.StartsWith(FULL_NAME_PR))
{
var path = AnimationUtility.CalculateTransformPath(_currentTrans, _rootTrans);
if (!string.IsNullOrEmpty(path))
{
var goWarp = new GUIFullPathGameObjectWrap();
goWarp.fullPath = path;
goWarp.realKey = GetFullPathNodeRealName(path, _currentTrans.name);
_rootNode.nodeList.Add(goWarp);
Debug.Log($"Go {_currentTrans.name} ==>> RealName: {goWarp.realKey} Path: {goWarp.fullPath} ");
}
}


var childNum = _currentTrans.childCount;
for (var i = 0; i < childNum; i++)
{
LoopCreateFullPath(_currentTrans.GetChild(i), _rootTrans, _rootNode);
}
}


private string GetFullPathNodeRealName(string _path, string _nodeName)
{
var splitStringList = _path.Split('/');
if (_nodeName.StartsWith(SHORT_NAME_PR))
{
return DoGetRealName(splitStringList[splitStringList.Length - 1]);
}

var realName = "";
for (var i = 0; i < splitStringList.Length; i++)
{
realName += DoGetRealName(splitStringList[i]) + "_";
}

//去掉尾部的"_"
return realName.Substring(0, realName.Length - 1);
}


private string DoGetRealName(string _name)
{
if (_name.StartsWith(SHORT_NAME_PR) || _name.StartsWith(FULL_NAME_PR))
{
return _name.Substring(2, _name.Length - 2);
}

return _name;
}

这部分代码同时也包括了对S_F_开头节点的解析. 其核心的遍历逻辑是调用

1
AnimationUtility.CalculateTransformPath(_currentTrans, _rootTrans);

注意该函数仅可以在编辑器模式下调用.

坚持原创技术分享,您的支持将鼓励我继续创作!