笨木头  2019-12-05 14:32     ECS入门     阅读(6101)     评论(4)
转载请注明,原文地址: http://www.benmutou.com/archives/2837
文章来源:笨木头与游戏开发
在ECS中,查询实体数据是非常重要的,也是我们不断在做的事情,之前有简单地提到了EntityQuery,这次我们来详细地解释。

首先,EntityQuery相当于是保存筛选条件的对象,然后从所有的实体中筛选满足条件的数据。EntityQuery提供了多种筛选数据的方式,接下来,先回忆之前提到过的最简单的使用方式。

1.组件

我们来创建三个组件:
public struct ComponentA : IComponentData { }

public struct ComponentB : IComponentData { }

public struct ComponentC : IComponentData { }
 

这三个组件没有任何字段,很简单。

至于创建实体的代码,我就不展示了。

2.系统

接下来看看最简单的EntityQuery的使用方式,下面是System代码:
using Unity.Entities;
using Unity.Jobs;

public class System_EntityQuery : JobComponentSystem { private EntityQuery m_query;

protected override void OnCreate () { base.OnCreate();

m_query = GetEntityQuery(typeof(ComponentA), typeof(ComponentB)); }

struct EntityQueryJob : IJobForEach<ComponentA> { public void Execute (ref ComponentA c) { } }

protected override JobHandle OnUpdate (JobHandle inputDeps) { return new EntityQueryJob().Schedule(m_query, inputDeps); } }
 

我们通过GetEntityQuery(typeof(ComponentA), typeof(ComponentB))获取了一个EntityQuery对象,这个对象指明了要筛选同时包含ComponentA和ComponentB组件的实体。

然后在调用Job的Schedule函数时,把EntityQuery对象传递进去。

于是我们在EntityQueryJob中能获取的实体必定是包含ComponentA和ComponentB组件的。

 

也许大家会觉得,IJobForEach不也可以筛选特定组件的实体吗?

没错,通过给IJobForEach指定泛型,如:IJobForEach<ComponentA, ComponentB>,我们也能达到同样的效果。

但是,EntityQuery更加灵活,因为它能满足更复杂的筛选方式。

3.AllAnyNone

接下来, 我们来看看EntityQuery更强大的筛选方式,直接看代码:
    protected override void OnCreate ()
    {
        var query = new EntityQueryDesc
        {
            All = new ComponentType[] { typeof(ComponentA), ComponentType.ReadOnly<ComponentB>() }
        };

m_query = GetEntityQuery(query); }
 

以上是创建EntityQuery对象的代码,这次我们先创建了一个EntityQueryDesc对象,然后再将EntityQueryDesc对象传给GetEntityQuery来创建EntityQuery对象。

在创建更复杂的EntityQuery对象时,就需要用到EntityQueryDesc,上面代码中,EntityQueryDesc有一个All字段,我们给All字段赋值了一个ComponentType数组,代表要筛选同时包含这个数组里所有类型的组件的实体。

 

其中,ComponentType.ReadOnly代表这个组件筛选出来之后是只读的。

因此,以上代码创建的EntityQuery将筛选出同时包含ComponentA和ComponentB的实体,并且筛选出来的ComponentB组件只能进行读取操作。

 

除了All以外,还有两种字段——Any和None

Any:实体包含指定的任意组件类型即可满足筛选条件

None:实体不可包含指定的任意组件类型

 

All、Any、None可以一起使用,如:
    protected override void OnCreate ()
    {
        var query = new EntityQueryDesc
        {
            All = new ComponentType[] { typeof(ComponentA), typeof(ComponentB) },
            Any = new ComponentType[] { typeof(ComponentC), typeof(ComponentD) },
            None = new ComponentType[] { typeof(ComponentE) }
        };

m_query = GetEntityQuery(query); }
 

以上代码创建的EntityQuery将筛选出满足以下条件的实体:同时包含ComponentA和ComponentB组件,且包含ComponentC或ComponentD中的其中一个组件,且不能包含ComponentE组件。

4.SetSharedComponentFilter

假设我们有一个共享组件SharedComponentA,它有一个字段:int num。

 

我们希望筛选出num = 1的那些共享组件的实体,那么,我们可以这么做:
    protected override void OnCreate ()
    {
        m_query = GetEntityQuery(typeof(SharedComponentA));

m_query.SetSharedComponentFilter(new SharedComponentA { num = 1 }); }
 

首先创建一个EntityQuery,并且筛选条件是包含SharedComponentA。

然后,调用SetSharedComponentFilter函数指定更细致的筛选条件:SharedComponentA的num字段必须等于1。

 

SetSharedComponentFilter函数可以在任何时候调用,并不仅限于OnCreate函数,比如你可以在OnUpdate调用。

至于为什么只有共享组件才能做这种筛选,我暂时没有找到相关的说明。

5.SetChangedVersionFilter

再来看一段代码:
    protected override void OnCreate ()
    {
        m_query = GetEntityQuery(typeof(ComponentA), typeof(ComponentB));
        m_query.SetChangedVersionFilter(typeof(ComponentA));
    }
 

上面的代码是什么意思呢?

它代表:需要筛选同时包含ComponentA和ComponentB的实体,并且,只有ComponentA的内容发生了改变的那些实体。

这样的话,我们就可以只对那些修改过的实体进行操作了。

 

但是,要注意,这里有一些坑。

"某个组件被修改过"这句话,和我们正常的理解有点出入。

这里指的是:该组件在其他System中被筛选了且被标记为读写。

 

什么意思呢,比如某个System执行了下面这个Job:



这个Job筛选了ComponentA,并且使用了ref标记,代表是读写操作。

 

那么,不管Execute函数内是否对ComponentA进行了赋值操作,ComponentA都算是"被修改"了。

也因此,ComponentA通过了SetChangedVersionFilter(typeof(ComponentA))的筛选。

 

并且,这种标记为"被修改"的状态,是会影响到整个块(Chunk)的。

 

比如上面这个System2_EntityQuery,它筛选了包含ComponentA的实体,并且ComponentA标记为可写。

那么,该System的筛选出来的所有实体,其所在的块(Chunk)都会被标记为"ComponentA已修改"。

 

我的描述有点乱,原文是这样的:
Note that for efficiency, the change filter applies to whole chunks not individual entities. The change filter also only checks whether a system has run that declared write access to the component, not whether it actually changed any data. In other words, if a chunk has been accessed by another Job which had the ability to write to that type of component, then the change filter includes all entities in that chunk. (This is another reason to always declare read only access to components that you do not need to modify.)
也因此,当我们不需要写入操作时,记得把组件标记为只读,否则会有不良的影响。

 

好了,关于EntityQuery,就介绍到这吧。

 

注意,本系列教程基于DOTS相关预览版的Package包,是预览版,不代表正式版的时候也适用。

转发本系列文章的朋友请注意,带上原文链接和原文日期,避免误导未来使用正式版的开发者。

4 条评论
  • sneaking 2020-07-11 11:38:13

    API改了,性能也提高了一些,原先40-55帧的例子在unity2020+最新版DOTS能跑到85-110帧.
    0回复
    • 博主 笨木头 2020-07-13 18:18:28

      那真的很牛了,不知道API使用上有没有更简单一些
      0回复
  • sunshuo 2020-05-08 01:23:46

    教程写的非常好,把ECS的使用方法说的很清楚。
    这么看unity ECS就是一个缩小版本的hadoop。
    首先这么编程很反人性,正常思考问题是某个事件触发某个逻辑然后再触发某个逻辑等等。
    这样就变成在所有时间点,所有逻辑要处理哪些数据。
    如果1千个人有1千个哈姆雷特,执行和开发的效率就会跑到另一个极端。
    0回复
    • 博主 笨木头 2020-05-08 06:51:43

      执行效率先不评论,开发模式确实很反人类,目前大家还是推荐把ECS用于有性能问题的部分,全部用ECS还是太折腾了。
      其实我一直在等project tiny,但tiny只能用ECS开发,又非常纠结,很多常用的工具要结合ECS一起用就非常尴尬。

      很久没研究ECS,据说现在到了0.9版本了(我研究的时候是0.3),不知道有多少改进。
      0回复
发表评论
粤ICP备16043700号

本博客基于 BlazorAnt Design Blazor 开发