TheLudGamer 发表于 2022-5-18 21:43

Unity中的GameObject查找

Unity中的GameObject查找

系列:Unity: 1 Million Traps and Pitfalls
在Unity中经常有根据名字查找GameObject的需求,这些需求有不同的实现方式,比如常见的GameObject.Find和Transform.Find。这些API互相相似,但是又有不同的使用方式。所以这里使用调研的方式,明确它们的功能,并提出其他查找方案的实现思路。
本文使用的Unity版本为2021.2.0。
1. 在打开的场景里查找GameObject

1.1 GameObject.Find

public static GameObject Find(string name);
GameObject类下公有的静态函数,至于具体功能,我们可以引用一下Unity文档的内容:
Finds a GameObject by name and returns it.
This function only returns active GameObjects. If no GameObject with name can be found, null is returned. If name contains a '/' character, it traverses the hierarchy like a path name.
For performance reasons, it is recommended to not use this function every frame. Instead, cache the result in a member variable at startup. or use GameObject.FindWithTag.
Note: If you wish to find a child GameObject, it is often easier to use Transform.Find.
Note: If the game is running with multiple scenes then Find will search in all of them. 正如文档上所说,是通过name这个参数,GameObject.Find会在所有运行着的场景中寻找处于Active状态的游戏对象。可以传入字符串作为参数name,如果参数中带有'/’,则被视为是类似文件名的层级对象名称来参与查找。
当然看上面这个描述,很多地方还是比较模糊的,这里实践之后来明确一下。
首先是每一个GameObject有两个和active状态有关的属性,分别是activeSelf,和activeInHierarchy,前者代表这个GameObject节点自己是Active的;后者代表它在所处的层级中都是激活的,也就是说它的父节点,它父节点的父节点...一直到场景下面的根节点全都是激活状态。
而官方文档里的说的returns active GameObjects,其实是只能查找并返回activeInHierarchy为true的节点。也就是如果查找的节点,其任何祖先节点都没有隐藏,才能返回它们,否则只会返回null。
第二个关键点就是Unity场景中的多个GameObject可以有共同的名字,但是GameObject.Find只会返回一个,那么这种情况下查找的规则是怎样的呢?
这就和两个同名GameObject在Hierarchy的顺序有关了,无论同名的GameObject是同级节点还是互相嵌套的节点,在视图中顺序靠上者会被优先选出。比如下图,图例中两个名为A的GameObject,上面的那个会被查找函数作为结果返回。



同名查找的场景测试用例

第三个关键点就是name的具体含义。name参数可以是对查找路径中一部分层级关系的描述,也可以是全部的。



查找C的场景测试用例

比如我们要在上图的场景里查找名为C的GameObject,以下三种Find方法中的name实参都是可以的:
GameObject c0 = GameObject.Find("A/B/C");
GameObject c1 = GameObject.Find("B/C");
GameObject c2 = GameObject.Find("C");
只要name所描述的层级确实存在即可。
最后就是name中用于对层级进行描述的特殊符号’/’,在GameObject的name中其实也可以有这个符号,那查找带有这个符号的name时,由于Find的行为只会把’/’符号解释为分割符而非名字的一部分,所以这种情况是无法查找的,只能返回null了。
1.2 Transform.Find

public Transform Find(string n);
Transform类下公有函数,Unity文档对此函数的描述如下:
Finds a child by name n and returns it.
If no child with name n can be found, null is returned. If n contains a '/' character it will access the Transform in the hierarchy like a path name.
Note: Find does not perform a recursive descend down a Transform hierarchy.
Note: Find can find transform of disabled GameObject.
所以这个函数的功能是基于某个对象的Transform组件,在这个组件的下面进行子组件的查找。
Transform是Unity的GameObject-组件系统中一个特殊的组件以致于直接成为了GameObject类型的字段。Transform组件不可重复、不可移除,除了位置大小缩放,这个组件还持有父子关系有关的数据。
这里Transform.Find和GameObject.Find行为的主要区别是:

[*]不支持递归查找,参数n是调用对象下的绝对的路径。
[*]无论或者查找的目标对象或者它们之间的任何对象有任何对象处于隐藏(非Active)状态,都可以进行查找。
Transform.Find的其他查找行为和GameObject.Find类似。
2. 辨析GameObject.GetComponent系列接口

当查找到对象之后,一般还需要查找对象自身的组件和子对象的组件。Unity提供了一些接口来根据对象来查询和对象有关的组件,下面是几个接口的函数原型:
public Component GetComponent(Type type);
public Component GetComponentInChildren(Type t, bool includeInactive);
public Component GetComponentInParent(Type t, bool includeInactive);
当然,这些接口也有其他的变体,这里只关注这三个常用的接口,因为查找行为和GameObject.Find有些类似,这里就一并总结了。
GetComponent函数只查找自身拥有的组件,如果查找不到会直接返回null。GetComponentInChildren函数和GetComponentInParent仍然先查找自身有无组件,如果没有分别向父级/子级进行查找。
Unity中GameObject的父子关系是一对多的,一个父节点可以有多个子节点。这时候GetComponentInChildren函数和GetComponentInParent的行为是深度遍历的,从离当前调用的GameObject最近的父/子节点开始遍历每个GameObject是否含有目标组件,一旦含有,就会把组件进行返回。
3. 一种最佳实践的思路

Unity的GameObject.Find接口无法查找非Active节点,Transform.Find接口不支持“相对路径”式的模糊匹配,两者都不支持对含有’/’符号的GameObject查找。通常在工程上,我们并不直接使用这两个需要仔细辨析功能,并且不适用于所有条件的接口来查找对象,而是在研究其行为后,自己封装一个统一的接口替代它们。
Unity提供了获取Active场景下,所有根GameObject的函数:
SceneManager.GetActiveScene().GetRootGameObjects()
我们可以把用此函数先获取到所有根节点,然后也同样进行递归式深度遍历。这样无论GameObject是否Active都可以查询到,同时自己维护匹配的路径数组,来模糊匹配效果。
对参数的处理上,我们可以把参数设置为string[]或者List类型来规避分割符号的存在、或者对分割符号进行转义、也可以让函数支持不同的分割符号来规避路径解析的问题。
参考


[*]^网络上也有一些博文说需要调用函数对象是Active的才能查找。但是本文在2021.2.0环境下对查找行为进行了较为全面的测试,并没有发现这种情况。推测之前的版本,查找行为可能确如其他博文所说。但是最新的版本里,查找行为已经改变了。
页: [1]
查看完整版本: Unity中的GameObject查找