Unity Entity Component System 4 --- System 系统

Systems

System在ECS中执行逻辑,将component data从一个状态变换到下个状态,比如一个系统,可以对所有移动实体执行运算,根据其方向,速度,时间计算新的位置。
ECS支持多种不同的systems,你可以实现ComponentSystem和JobComponentSystem来为entities执行逻辑。这两种Systems可以容易地选择和遍历拥有指定components的entities。
系统提供时间类型的回调,像OnCreate()和OnUpdate(),可以实现相关系统生命周期的代码。这些方法是在主线程中调用的。在Job Component System中,你还可以在OnUpdate()中Schedule Jobs,Jobs本身是运行在工作线程(相对于主线程)中的。通常,Job Component Systems的性能更好,因为它能充分利用多核优势。如果Jobs以Burst编译,则能进一步提升性能。
ECS会自动发现System类,并在运行时创建。Systems被World以Group组织。你可以通过System Attributes控制System被添加到哪个Group,以及在该Group中它们以什么样的顺序执行。默认的,所有的systems被创建到default world的Simulation System Group中,但是为指定执行顺序。你可以同构system attribute禁止自动创建Systems。
系统的执行,由父Component System Group驱动。一个Component System Group本身也是个System,用来执行子Systems。
可以查看运行时System配置信息(菜单: Window > Analysis > Entity Debugger)。

System event functions

通过实现一组System的生命周期回调接口,来管理生命周期。ECS按照下面的顺序调用相关接口:

  • OnCreate() 系统创建时调用
  • OnStartRunning() 第一次调用OnUpdate之前,以及每次回复执行时调用
  • OnUpdate() 当系统是Enabled状态,并且有任务时(参考ShouldRunSystem()),每帧调用。OnUpdate函数定义在基类ComponentSystemBase中,所有类型的system都可以实现自己的逻辑行为。
  • OnStopRunning() 当System的Query没有匹配到entities,或者系统调用OnDestroy()前,被调用。
  • OnDestroy() 系统销毁时调用。
    以上函数都运行在主线程中。可以在JobComponentSystem类System的OnUpdate中调度Jobs来执行多线程任务。
System Types

ECS提供了几种Systems类型。实现游戏逻辑行为的系统,通常派生类ComponentSystem或者JobComponentSystem。其它的系统有特殊用途,比如可以用Command Buffer System 和 Component System Group类的实例。

  • Component Systems 实现该基类,在主线程中执行游戏逻辑,也可以使用非ECS优化的Jobs(直接使用Job System)。
  • Job Component Systems 实现该基类,可以使用IJobForEach或者IJobChunk来执行逻辑。
  • Entity Command Buffer Systems 提供EntityCommandBuffer实例给其它系统使用。每个默认的system groups,在其子系统列表的头尾,都有一个EntityCommandBufferSystem。
  • Component System Groups 为其它系统提供管理和更行顺序控制。默认ECS会创建几个Component System Groups。

Component Systems

Component System 执行entities的逻辑。它不能包含数据。可以看作老版本的Component,只是它只有方法,没有数据。一个Component System负责更新所有的匹配的entities(由EntityQuery进行匹配)。
ComponentSystem是个纯虚类,通过实现相关接口执行我们的逻辑。

JobComponentSystem

Automatic job dependency management

自动管理Job依赖。
管理Job的依赖是件很麻烦的事。而JobComponentSystem帮我们自动处理了。规则很简单:不同系统的Jobs可以并行地访问同样类型的IComponentData。但是如果有一个Job在向该数据写操作,则这些Jobs不能并行执行,必须根据依赖调度Jobs。

public class RotationSpeedSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedRotation : IJobForEach<Rotation, RotationSpeed>
    {
        public float dt;

        public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
        {
            rotation.value = math.mul(math.normalize(rotation.value), quaternion.axisAngle(math.up(), speed.speed * dt));
        }
    }

    // Any previously scheduled jobs reading/writing from Rotation or writing to RotationSpeed 
    // will automatically be included in the inputDeps dependency.
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotationSpeedRotation() { dt = Time.deltaTime };
        return job.Schedule(this, inputDeps);
    } 
}
How does this work?

所有这些Jobs和Systems声明了它们要读或写的ComponentType,因此当JobComponentSystem返回JobHandle时,它自动将对这些ComponentType的读写访问注册到EntityManager中。
因此当一个系统向A写操作,其它系统之后读取A,那么JobComponentSystem会检查列表中的读取操作,并将传递一个对第一个系统的依赖。
JobComponentSystem简单地将Jobs按照依赖关系连接到一起,简化了我们在主线程中的依赖关系管理。但是,如果一个非Job的ComponentSystem要访问这些数据呢?因为所有的数据访问时事先声明好的,因此ComponentSystem会在与其它有共同依赖的component的所有的jobs完成后,再调用它的OnUpdate。

Dependency management is conservative & deterministic

依赖管理是保守且确定的
依赖管理倾向于保守。ComponentSystem简单地跟踪用到过的EntityQuery对象并记录它们对组件类型的读写配置。
而且,当在一个System中调度多个jobs时,无论这些Jobs是否是依赖的,都要讲依赖对象传递给它们。如果证明这导致效率问题,那么尝试把该系统拆分成2个系统。
依赖管理被设计的保守,这样能够用简单的API保证逻辑的确定性和正确性。

Sync points

所有对结构的改变,都会导致“强制同步点”,如,CreateEntity,Instantiate,Destroy,AddComponent,RemoveComponent,SetSharedComponentData。这意味着这些调用发生时,之前通过JobComponentSystem调度的所有的Jobs都要完成。这是自动的。比如,在逻辑帧的执行中间,调用EntityManager.CreateEntity,会导致逻辑停下来,等待之前的Jobs 完成。
可以通过EntityCommandBuffer来避免实体创建类操作导致的Sync Points。

Multiple Worlds

每个World都有自己的EntityManager,以及互相独立的JobHandle依赖管理。一个World的Hard Sync Point,不会对另一个World产生影响。因此,对于序列化和创建对象,一个有用的建议是,在一个World中创建对象,创建好后,在一帧的开始将他们移动到另一个World中。
可以参考System update order 以及 ExclusiveEntityTransaction来避免实体创建和序列化导致的Sync Points。

Entity Command Buffer

EntityCommandBuffer解决了2个问题:

  1. 当你再Job中时,不可以访问EntityManager
  2. 当访问EntityManager时(比如,创建一个Entity)导致导致访问的Components数组失效,及EntityQuery对象失效。
    Entity Command Buffer抽象允许我们将改变保存到队列(从Job或者主线程),然后在稍后在主线程中应用。有2中应用EntityCommandBuffer的方法:
    ComponentSystem的子类PostUpdateCommands可以在主线程中执行。可以简单地调用方法来将改变队列化,当world执行完该系统的Update后,会立即应用它们。
    例子:
PostUpdateCommands.CreateEntity(TwoStickBootstrap.BasicEnemyArchetype);
PostUpdateCommands.SetComponent(new Position2D{Value= spawnPosition});
PostUpdateCommands.SetComponent(new Heading2D{Value = new float2(0.0f,-10f)});
PostUpdateCommands.SetComponent(default(Enemy));
PostUpdateCommands.SetComponent(new Helath{Value=TwoStickBootstrap.Settings.enemyInitialHealth});
PostUpdateCommands.SetComponent(new EnemyShootState{Cooldown=0.5f});
PostUpdateCommands.SetComponent(new MoveSpeed(speed=TwoStickBootstrap.Setting.enemySpeed));
PostUpdateCommands.AddSharedComponent(TwoStickBootstrap.EnemyLook);

对Jobs来说,必须在主线程中从entity command buffer system请求EntityCommandBuffer,然后传递给jobs。当EntityCommandBufferSystem更新时,command buffer会在主线程中按照创建的顺序执行它们。

Entity Command Buffer Systems

默认World初始化时创建了3个system groups,用来初始化,模拟,显示,它们在每一帧顺序执行。在每个group中,有一个entity command buffer system,在groups内所有其它system执行前执行一次,并且在所有其它system执行完后再执行一次。我们建议使用已有的command buffer systems,而不是自己创建,这样可以最小化同步点数量(Command buffer system越多,Sync Points越多)。

Using EntityCommandBuffers from ParallelFor jobs

当在ParallelFor jobs中使用EntityCommandBuffer来执行EntityManager命令时,EntityCommandBuffer.Concurrent接口可以保证线程安全和执行的确定性。该方法需要一个额外的jobIndex作为参数,用来保证命令回放执行的顺序是确定的。JobIndex必须是每个job唯一的。为了提升效率,jobIndex应该是传递给IJobParallelFor.Execute()的自增的index值。除非你帧地了解它们的顺序关系,否则用Execute()的index 参数是最佳选择。使用其它的jobIndex即使正确执行了,也可能带来效率方面的其它问题。

System Update Order

通过Component System Groups来指定systems的执行顺序。可以通过在系统的类的定义上使用[UpdateInGroup] Attribute来指定该系统在哪个group中执行。还可以使用[UpdateBefore] [UpdateAfter]来指定在该Group内的执行顺序。
ECS框架创建了一组默认的system groups,可以利用这些groups来控制我们的系统的执行顺序。还可以将一个Group创建到另一个Group内,这样来进一步控制系统的执行顺序(Group是可以嵌套的)。

Component System Groups

ComponentSystemGroup类组织若干相关的Systems到一起,并以一定的顺序执行。ComponentSystemGroup是ComponentSystemBase的派生类,因此它具有一个componenty system应该有的特性,可以跟其它系统关联,有OnUpdate方法,等。最主要的,这意味着Component System Croups可以嵌套到其它Component System Group中,建立层次关系。
默认地,当ComponentSystemGroup的Update调用时,它会调用所有保存的系统列表里的系统的Update()。如果一个子Component System是group时,会递归调用它的子系统列表,按照深度优先遍历整棵层次树。

System Ordering Attributes

目前系统维护了一套顺序Attributes:

  • [UpdateInGroup] — 为该系统指定是哪个group的成员。如果没有该修饰,默认会添加到World的SimulationSystemGroup中。
  • [UpdateBefore] [UpdateAfter] — 控制组内系统的相对执行顺序。使用这些修饰的系统类型,必须在同一个group中。对于跨group的顺序,按照包含2个systems的最深的group决定:
  • 例如:SystemA在GroupA,SystemB在GroupB,GroupA和GroupB都在GroupC,那么SystemA和SystemB的隐含顺序为Group A和GroupB的顺序。
  • [DisableAutoCreation] — 禁止默认 World初始化时自动创建System。之后需要自己明确的创建和更新这些系统。当然,也可以创建完系统后,添加到一个ComponentSystemGroup中,那么系统的Update会被Group自动调用。
Default System Groups

默认的World包含了一个ComponentSystemGroup实例的层级树。仅三个根级system groups被添加到Unity的逻辑循环中。下面的列表展示了这三个组中预定义的子系统:

  • InitializationSystemGroup(在主循环的Initialization阶段结束时执行)
    • BeginInitializationEntityCommandBufferSystem
    • CopyInitialTransformFromGameObjectSystem
    • SubSceneLiveLinkSystem
    • SubSceneStreamingSystem
    • EndInitializationEntityCommandBufferSystem
  • SimulationSystemGroup(主循环的Update结束时执行)
    • BeginSimulationEntityCommandBufferSystem
    • TransformSystemGroup
      • EndFrameParentSystem
      • CopyTransformFromGameObjectSystem
      • EndFrameTRSToLocalToWorldSystem
      • EndFrameTRSToLocalToParentSystem
      • EndFrameLocalToParentSystem
      • CopyTransformToGameObjectSystem
    • LateSimulationSystemGroup
    • EndSimulationEntityCommandBufferSystem
  • PresentationSystemGroup(主循环的PreLateUpdate结束时调用)
    • BeginPresentationEntityCommandBufferSystem
    • CreateMissingRenderBoundsFromMeshRenderer
    • RenderingSystemBootstrap
    • RenderBoundsUpdateSystem
    • RenderMeshSystem
    • LODGroupSystemV1
    • LodRequirementsUpdateSystem
    • EndPresentationEntityCommandBufferSystem

这个列表的内容目前还在优化中,会发生变化。

Multiple Worlds

可以创建除了默认World的多个其它Worlds,或者替换默认World。同样的Component System可以创建到多个World中,并且可以以不同的更新频率,在不同的时间点更新。
现在,不支持手动地更新World内的系统;你只能控制某个系统被创建到哪个World中的哪个System Group中。也即是,自定义的WorldB可以实例化SystemX和SystemY,并将SystemX添加到默认World的SimulationSystemGroup,将SystemY添加到默认World的PresentationSystemGroup中。这些Systems的顺序关系在它们所在的Group中被管理,并按照Group的顺序更新。
为了实现这种用法,提供了ICustomBootstrap 接口类:

public interface ICustomBootstrap
{
	List<Type> Initialize(List<Type> systems);
}

实现该接口时,一个完整的System类型的列表被传递到Initialize()方法中,而且它在默认World初始化前调用。自定义的启动加载器可以遍历这个列表并且创建需要的System。可以返回一个System列表,该列表内的系统将被默认World初始化。(不返回则不创建)
例如,下面是个典型的MyCustomBootstrap.Initialize()的实现过程:

  1. 创建一个World和它的最上层的ComponentSystemGroups。
  2. 对所有的System类型:
    a) 遍历ComponentSystemGroup层次,找到该System应该被添加到哪个Group中。
    b) 如果找到Group,在该World中创建System并调用group.AddSystemToUpdateList()接口添加到层次结构中。
    c) 如果没有找到要添加到的Group,那么,添加到返回的System类型列表中,返回到DefaultWorldInitialization。
  3. 调用所有顶层的groups的group.SortSystemUpdateList()。
    a) Optionally add them to one of the default world groups
  4. 返回未处理的系统的列表给DefaultWorldInitializtion。
    注:ECS框架通过反射用户自定义的ICustomBootstrap实现。

建议:

  • 用[UpdateInGroup]来指定你的系统在哪个Group中执行。如果不指定,默认是SimulationSystemGroup。
  • 用手动更新的ComponentSystemGroups来在Unity主循环的其它阶段更新Systems。为ComponentSystem或者ComponentSystemGroup添加[DisableAutoCreation]修饰来避免被自动添加到默认的SystemGroups中,可以手动地通过接口World.GetOrCreateSystem()来创建System并在主线程中调用MySystem.Update()来执行。这是将System添加到主循环其它阶段去执行的简单的办法。(例如,你可以创建一个系统,在一帧的前面或者后面执行。)
  • 尽可能的使用已有的EntityCommandBufferSystem,而不要自己创建。一个EntityCommandBufferSystem代表一个SyncPoint,主线程要等待所有工作线程的工作完成,以执行它的缓存的EntityCommandBuffers。重复利用预定义在顶层SystemGroup中的Begin/End Systems来避免SyncPoints,提升效率。
  • 避免将自定义逻辑写到ComponentSystemGroup.OnUpdate()中。因为ComponentSystemGroup本身是个ComponentSystem,让人想要将用户逻辑添加到它的OnUpdate()中来执行一些逻辑,创建Jobs,等。我们反对这么做,因为从表面上很难马上确定,它的Execute是在group‘s的其它成员System之前还是之后。最好就是让SystemGroups来执行grouping机制,而在其它的System中实现我们的逻辑,并明确地指定它们的相对执行顺序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值