ECS访问Entity数据

ECS访问Entity数据

ECS systems的主要任务就是读取一系列的components数据,计算以后将结果写入其他的components。那么如何高效地读写数据就是很重要的,而有效的方法就是利用cpu的多核特性来并行地读取和处理数据。

ECS提供了多种完成这个任务的方式,每一种都有自己的规则和约束:

API 说明
JobComponentSystem Entities.ForEach 最简单的方式
IJobForEach 相当于Entities.ForEach,但需要写更多代码
IJobForEachWithEntity 比IJobForEach稍微复杂点,提供了访问entity handle和entity array index
IJobChunk 基于chunk的访问
ComponentSystem.ForEach main thread工作模式
Manual iteration 手动遍历entities或chunks

EntityQuery:用来根据条件查询entities,上述的各种访问entities的方式,基本都显示或隐式地依赖EntityQuery。

WithStoreEntityQueryInField(ref EntityQuery)可用来访问隐式EntityQuery对象。

JobComponentSystem lambda 函数

两种方式的lambda表达式:

  • JobComponentSystem.Entities.ForEach(lambda):针对每个entity的job
  • JobComponentSystem.Job.WithCode(lambda):针对某一段逻辑代码的job
函数 说明
WithAll<T> 全包含
WithAny<T, U> 包含任意
WithNone<T> 不包含
WithChangeFilter<T> 1. 特定component变更时包含,和上一次update相比,目标component要么在参数列表中,要么在WithAny里面。一次最多同时支持2个components类型的变更。2. 基于chunk级别的变更,如果有代码以write的方式访问chunk中的component,也会被标记为changed,不管是否实际改变任何数据
WithSharedComponentFilter 含有特定值的shared component调用时传入shared component object
WithStoreEntityQueryInField 可以将EntityQuery对象提前显式地存在一个变量里面,在第JCS创建时赋值。第一次执行之前就可以使用了,可用来查询entities的数量

重要:不能将ForEach参数列表里面的components类型,重复放在WithAll、WithAny、WithNone里面。

Entities.ForEach lambda 函数参数列表:

  1. 最多8个参数
  2. 参数顺序规则:
    • 按值传递的参数(copy-by-value的方式拷贝)
    • 可写,ref参数
    • 只读,in参数
  3. 所有的components参数要么是ref要么是in
  4. 特殊固定名字参数:
    • Entity entity
    • int entityInQueryIndex
    • int nativeThreadIndex,通过Run()函数手动执行lambda函数时,始终为0

Capturing variables 变量修饰:

函数名 说明
WithReadOnly(myvar) 只读修饰
WithDeallocateOnJobCompletion(myvar) 释放native container(本地容器)
WithNativeDisableParallelForRestriction(myvar) 1. 保证多个线程访问同一个可写naive container。2.并行访问只有在每个线程都访问自己独有的数据(或native container中的某一段)时才是安全的。多个线程同时访问同一个额数据会导致race condition。
WithNativeDisableContainerSafetyRestriction(myvar) 禁用访问native container的安全限制。
WithNativeDisableUnsafePtrRestrictionAttribute(myvar) 允许使用unsafe指针。

Job选项:

  1. JobHandle Scedule(JobHandle):以job方式执行lambda
    • Entities.ForEach:在job threads上并行执行,每个job遍历query到多chunks上的所有的entities。
    • Job.WithCode:在一个job thread上执行一个lambda函数的实例。
  2. void Run():主线程同步执行lambda函数,不以job的方式运行
    • Entities.ForEach:lambda在每个query出来的chunks的entity上都执行一次。
    • Job.WithCode:lambda函数执行一次。
  3. WithBurst(FloatMode, FloatPrecision, bool):Burst编译器参数
    • floatMode:Default Deterministic Fase Strict
    • floatPrecision:High Low Medium Standard
    • synchronousCompilation:
  4. WithoutBurst():关闭Burst编译。当lambda表达式里面有不支持Burst的代码时使用。
  5. WithStructuralChanges():在主线程以关闭Burst的方式运行,这时候可以做一些structural changes的操作。建议使用EntityCommandBuffer来代替这种用法。
  6. WithName(string):给job起名字,方便调试的时候用。

不能在job中对entity做结构性变更,比如创建entity、添加或移除component、销毁entity等。正确的方式是使用ECB来缓存并延迟处理这些变更操作。

使用IJobForEach访问Entity

IJobForEach以chunk为单位处理entities,同一个chunk中的所有entities当做一个批次来处理。当entities分布在不同的chunk中是,不同chunk的处理是并行的。这样以chunk为单位的处理方式效率非常高,因为单个线程只处理自己的chunk中的数据,不会有多个线程同时处理一个chunk中的数据。

但是,如果每一个entity的处理都非常耗时,就有可能会带来性能问题,这种情况下可以使用IJobParallelFor来进行手动方式的遍历。

定义:

    [ExcludeComponent(typeof(Frozen))]
    [RequireComponentTag(typeof(Gravity))]
    [BurstCompile]
    struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
    {}

模板参数表示需要操作的components。

属性标签:

标签 说明
[ExcludeComponent(typeof(T))] 不包含
[RequireComponentTag(typeof(T))] 需要包含
[BurstCompile] Burst编译加速

执行函数Execute():

对于每个有效的entity,都会执行Execute()函数。

    public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed){}

参数列表必须和类型的模板参数列表匹配,同时可以使用以下修饰符:

标签 说明
[ReadOnly] 只读
[WriteOnly] 只写
[ChangedFilter] chunks中的component变更时处理chunks中所有的entities,否则就完全不处理

IJobForEachWithEntity:

提供额外的参数:Entity对象和index。

可以使用提供得entity对象结合ECB,来操作entity或添加删除components。

使用ComponentSystem访问Entity

首选可以利用CPU多核特性的JobComponentSystem,而ComponentSystem一般只会用在以下几种情况:

  1. 调试或测试性开发;
  2. 当需要调用只能在main thread上调用的一些API时使用;
  3. 代码的复杂甚至低于创建和调度一个job时使用;
  4. 当需要在遍历entities时直接执行一些结构性修改时,可以直接在ForEach中同步执行。

注意:在主线程的结构性修改会同步等待所有的jobs执行完成,所以会造成卡顿。这种情况下可以使用一个post-update command buffer,提前收集所有的修改操作,然后在某个时间点一次执行。

注意:ForEach lambda最多支持6个类型的components参数。

使用IJobChunk访问Entity

JobComponentSystem会针对每一个chunk执行一遍Execute()函数,然后在函数中逐个entity处理。

比IJobForEach代码更复杂,需要更多代码。但是要更加的明确和直接地访问数据。

使用chun还有一个方便之处就是,可以在代码中检查optionl component是否存在,然后细化处理。

  1. 创建EntityQuery

    使用JobComponentSystem.GetEntityQurey()来创建EnityQuery;

    一个system定义一次然后存成变量。

  2. 定义IJobChunk

    ArchetyChunkComponentType<T>:每个需要访问的component都需要定一个该类型对象,用来获得T的列表数组。在OnUpdate()中每帧赋值,然后在Job.Execute()中使用。

    注意:ArchetyChunkComponentType<T>不能缓存,必须每帧在使用之前更新。

  3. Eexcute()执行函数

    使用chunk.Has<T>来判断是否包含某个可选的组件,每个chunk在一次执行中只需要check一次;

    基于版本号是否变更来决定是否执行job;

  4. 实例化job、给字段赋值、并调度

public class TestSystem : JobComponentSystem
{
    private EntityQuery m_Query;
    protected override void OnCreate()
    {
        m_Query = GetEntityQuery(
            ComponentType.ReadWrite<Output>(),
            ComponentType.ReadOnly<InputA>(),
            ComponentType.ReadOnly<InputB>());
        m_Query.SetChangedVersionFilter(
            new ComponentType[]
            {
                ComponentType.ReadWrite<InputA>(),
                ComponentType.ReadWrite<InputB>()
            });
    }
 
 
    [BurstCompile]
    struct UpdateJob : IJobChunk
    {
        public ArchetypeChunkComponentType<InputA> InputAType;
        public ArchetypeChunkComponentType<InputB> InputBType;
        [ReadOnly] public ArchetypeChunkComponentType<Output> OutputType;
        public uint LastSystemVersion;
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var inputAChanged = chunk.DidChange(InputAType, LastSystemVersion);
            var inputBChanged = chunk.DidChange(InputBType, LastSystemVersion);
            // If neither component changed, skip the current chunk
            if (!(inputAChanged || inputBChanged))
                return;
            var inputAs = chunk.GetNativeArray(InputAType);
            var inputBs = chunk.GetNativeArray(InputBType);
            var outputs = chunk.GetNativeArray(OutputType);
            for (var i = 0; i < outputs.Length; i++)
            {
                outputs[i] = new Output{ Value = inputAs[i].Value + inputBs[i].Value };
            }
        }
    }
 
    protected override JobHandle OnUpdate(JobHandle inputDependencies)
    {
        var job = new UpdateJob();
        job.LastSystemVersion = this.LastSystemVersion;
        job.InputAType = GetArchetypeChunkComponentType<InputA>(true);
        job.InputBType = GetArchetypeChunkComponentType<InputB>(true);
        job.OutputType = GetArchetypeChunkComponentType<Output>(false);
        return job.Schedule(m_Query, inputDependencies);
    }
}

手动迭代遍历Entity

在JobComponentSystem中使用IJobParallelFor:

public class RotationSpeedSystem : JobComponentSystem
{
   [BurstCompile]
   struct RotationSpeedJob : IJobParallelFor
   {
       [DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> Chunks;
       public ArchetypeChunkComponentType<RotationQuaternion> RotationType;
       [ReadOnly] public ArchetypeChunkComponentType<RotationSpeed> RotationSpeedType;
       public float DeltaTime;
       public void Execute(int chunkIndex)
       {
           var chunk = Chunks[chunkIndex];
           var chunkRotation = chunk.GetNativeArray(RotationType);
           var chunkSpeed = chunk.GetNativeArray(RotationSpeedType);
           var instanceCount = chunk.Count;
           for (int i = 0; i < instanceCount; i++)
           {
               var rotation = chunkRotation[i];
               var speed = chunkSpeed[i];
               rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.RadiansPerSecond * DeltaTime));
               chunkRotation[i] = rotation;
           }
       }
   }
   EntityQuery m_Query;   
   protected override void OnCreate()
   {
       var queryDesc = new EntityQueryDesc
       {
           All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>() }
       };
       m_Query = GetEntityQuery(queryDesc);
   }
   protected override JobHandle OnUpdate(JobHandle inputDeps)
   {
       var rotationType = GetArchetypeChunkComponentType<RotationQuaternion>();
       var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed>(true);
       var chunks = m_Query.CreateArchetypeChunkArray(Allocator.TempJob);
       var rotationsSpeedJob = new RotationSpeedJob
       {
           Chunks = chunks,
           RotationType = rotationType,
           RotationSpeedType = rotationSpeedType,
           DeltaTime = Time.deltaTime
       };
       return rotationsSpeedJob.Schedule(chunks.Length,32,inputDeps);
   }
}

遍历所有的entities:

var entityManager = World.Active.EntityManager;
    var allEntities = entityManager.GetAllEntities();
    foreach (var entity in allEntities)
    {
       //...
    }
    allEntities.Dispose();

遍历所有的chunks:

var entityManager = World.Active.EntityManager;
    var allChunks = entityManager.GetAllChunks();
    foreach (var chunk in allChunks)
    {
       //...
    }
    allChunks.Dispose();

EntityQuery的用法

最简单的方式:

在System内部创建:

    EntityQuery m_Query = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());

在System外部创建:

    EntityQuery m_Query = CreateEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());

使用EntityQueryDesc:

var query = new EntityQueryDesc
    {
       None = new ComponentType[]{ typeof(Frozen) },
       All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>() }
       Options = EntityQueryDescOptions.FilterWriteGroup
    }
    EntityQuery m_Query = GetEntityQuery(query);

查询选项:(一般不需要设置)

选项 说明
Deault 不设置
IncludePrefab 包含特定Prefab tag的compnent
IncludeDisable 包含特定Disabled tag的component
FilterWriteGroup query时考虑component的WirteGroup设置

组合查询:

var query0 = new EntityQueryDesc
    {
       All = new ComponentType[] {typeof(RotationQuaternion)}
    };
    var query1 = new EntityQueryDesc
    {
       All = new ComponentType[] {typeof(RotationSpeed)}
    };
    // query0或query1
    EntityQuery m_Query = GetEntityQuery(new EntityQueryDesc[] {query0, query1});

使用filter:

  1. Shared component filters:筛选出拥有特定值的sharedcomponent的entities

    最多同时支持2个shared component filters

    struct SharedGrouping : ISharedComponentData
        {
            public int Group;
        }
        class ImpulseSystem : ComponentSystem
        {
            EntityQuery m_Query;
            protected override void OnCreate(int capacity)
            {
                m_Query = GetEntityQuery(typeof(Position), typeof(Displacement), typeof(SharedGrouping));
            }
            protected override void OnUpdate()
            {
                // Only iterate over entities that have the SharedGrouping data set to 1
                m_Query.SetFilter(new SharedGrouping { Group = 1 });
                var positions = m_Query.ToComponentDataArray<Position>(Allocator.Temp);
                var displacememnts = m_Query.ToComponentDataArray<Displacement>(Allocator.Temp);
                for (int i = 0; i != positions.Length; i++)
                    positions[i].Value = positions[i].Value + displacememnts[i].Value;
            }
        }
    
  2. Change filters:

        protected override void OnCreate(int capacity)
        {
            m_Query = GetEntityQuery(typeof(LocalToWorld), ComponentType.ReadOnly<Translation>());
            m_Query.SetFilterChanged(typeof(Translation));
        }
    

使用查询结果:

函数 说明
ToEntityArray() 转为entities数组
ToComponentDataArray<T>() 转为T的数组
CreateArchetypeChunkArray() 转为Chunk数组

还可以直接将query传给job.Shedule()函数:

var job = new RotationSpeedJob()
    {
        RotationType = rotationType,
        RotationSpeedType = rotationSpeedType,
        DeltaTime = Time.deltaTime
    };
    return job.Schedule(m_Query, inputDependencies);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。 System...
    洪智阅读 595评论 0 0
  • 洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。 你可以在系统...
    洪智阅读 724评论 0 0
  • 洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在探索DOTS。 由于ECS概念过于复...
    洪智阅读 363评论 0 1
  • 本文主要总结Hybrid ECS基础用法,Pure ECS将会在之后进行补充。不详细描述ECS概念与原理。更多内容...
    悬崖边有点脚滑阅读 3,089评论 0 3
  • 洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。 Entiti...
    洪智阅读 430评论 0 0