Unity ECS and Traffic Simulation | UnityECS架构与交通模拟

尝试做交通和行人的模拟,自然就想到了Unity2018的新功能ECS+Job System,线性数据+多线程提高模拟速度,因此本文分两部分:ECS和交通模拟

以下为一个WIP的成果

基本上八核都跑满了,不知道是不是compiler的问题,有时候4k的车就30fps了,有时候10k的车还能40fps????

主要两个模块:生成RoadGraph和车辆模拟

RoadGraph部分,为了让Job里能调用,全都是array形式存的了,没有指针全是ID来指。

车辆模拟部分有四个Job,一个SetUp设置RaycastCommand,一个运行raycast,一个处理raycast的结果。最后一个更新vehicle的位置等数据。

Profile看Job里最多的是Raycast物理。这也是比较头疼的,最开始是Pure ECS实现的,写写着发现没法加物理啊,只能变成Hybrid ECS,实例化GameObject加上Collider,还好更新collider的位置开销不大,主要是raycast开销大,用于agent感知周边车辆位置。不这么做的话怕是得自己弄一套BVH,那样估计pure ECS可能可以优化一些速度。

代码都挂在Github上了,见OSMTrafficSim

下面讲一讲ECS的理解吧

Unity ECS

ECS是Entity-Component-System的简称,2018推出的新功能。是一种比较新的框架体系,主要优点是
  • 处理大量物体时性能较好,data-layout对cache友好,便于多线程计算。
  • 耦合性低,代码清晰。Component只有数据没有逻辑,System只有逻辑没有状态。

Unity传统框架是有Entity-Component思想的,但不是ECS这个Entity-Component。传统Unity中Gameobject是Entity,Monobehaivor是component挂在Gameobject上。这样数据分散在内存多处,用指针获取,这样Cache-Miss会很多。而且数据耦合严重,线程不安全。

ECS的数据基本都是按一维数组形式存储的,很容易一起放进cache,加快CPU计算速度。要知道内存-L3-L2-L1的数据读取花费的CPU指令周期是近似数量级降低的。用ID形式访问也对多线程比较友好,避免data-race。
笔者更关心计算性能的提升,最明显的是对大量物体的逻辑与渲染有帮助,比如Swarm的模拟,游戏中就可以做万人同屏等了。效率提升可能是数量级的。而对非大量物体的游戏中,提升有多少就不好说了。物理模拟可能是比较有帮助的,可以分配到多线程。其他的逻辑呢,可是本来开销就和物理和渲染不在同一个数量级的呀。

ECS的一些基本概念

Unity官方有一些介绍

World

UE中也有world,这次Unity也加上了。Play开始后默认创建一个World,你也可以自己创建。
每个World有一个EntityManager和很多的ComponentSystem。
EntityManager是管所有Entity的,ComponentSystem是System,管行为的,所有的Update都在里面。
Component(数据)挂在Entity下面。

ComponentSystem

用于逻辑的,要有Update。注意的是,System是默认进行Update的!Monobehavior需要挂到GameObject上才会Update,ComponentSystem只要你创建了这个代码就会Update的。
另外ComponentSystem不依赖与GameObject,Hierarchy窗口里是找不到ComponentSystem的,但它就一直在Update。

Entity, EntityManager

本身什么就没有,就是一个ID。EntityManager里面可以找到每个entity,在EntityDebug窗口中也能看到。Entity也是不依赖GameObject。World中可以有不挂在GameObject上的Entity,当然每个GameObject可以加GameObjectEntity变成Entity。
一般是用下面两个方法创建Entity。Archetype可以认为是一个类型,包含了需要哪些类型的Component。Entity就是Architype的实例,注入了数据

IComponentData

Component类的接口,所有存数据的地方。注意这个是个Struct,其元素必须是非托管的,blitable类型的。
比如bool,int[],NativeArray<>都不能在IComponentData中存在。
Unity.Mathematics里面的类都可以。
另外每个IComponentData一般会声明一个DataWrapper,这个Wrapper才是一个可以挂在Entity上面的Component。

ComponentGroup, Inject, SubtractiveComponent

这几个存在的意义在于:ComponentSystem的Update对于哪些Entity起作用
Inject对象声明后会在ComponentSystem的OnCreateManager()之前获取到需要参与计算的Entity
举个例子,其中SubtractiveComponent代表了,System关心的Entity不能有这些Component。
下面这个例子中,group会获取到所有有Position,有Rigidbody,但是没有MeshCollider的Entity

不用Inject的话,也可以ComponentGroup方法获得要参与计算的Entity。下面这个例子中,找到了所有包含Position, Rigidbody, SharedGrouping组件的Entity,注意ComponentGroup还有filter方法选择一部分Entity

Job

一般System里更新Entity的数据都是Job的方式,就是一个多线程的任务,方便CPU调度。
native方式,所以非托管的方式就跟写C++一样了,比如Job里想要一个定长int数组:
Malloc一下,最后要Free掉
所以一个ECS的基本逻辑是:

两个有意思的ECS示例项目

Voxelman,跳舞的体素小人
首先已经有了两个骨骼运动的小人,小人的模型会运行时烘焙成meshcollider
System首先设置好Voxel,然后RayCast那个小人的collider获得Voxel的位置,最后更新Voxel的位置,渲染。

uSpringBones,飘带物理运算的ECS版本。
这个就没有ComponentSystem,全是Monobehavior开Job更新,Component主要用于更新Transform位置。

参考资料

OSMTrafficSim,笔者项目的Github
官方ECS介绍,Github上这个比官网的全一些
Voxelman ,有意思的ECS项目
uSpringBones,有意思的ECS项目
All of the Unity ECS Job System gotchas so far 很多ECS的小tips

发表评论

电子邮件地址不会被公开。 必填项已用*标注