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

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

以下为一个WIP的成果

该项目的实现主要有几个模块:生成RoadGraph,构造BVH,车辆模拟,行人模拟。

RoadGraph部分,为了让Job里能调用,全都是Array形式存的了,没有指针全是ID来指。构造BVH和行人模拟在笔者另外的博客中讲到了。车辆模拟有三个部分,首先构造BVH,之后每个Vehicle遍历BVH感知周边信息,最后一个更新Vehicle的位置等数据。

最早考虑的是用物理raycast来做空间查找,让vehicle感知周边信息。先是想用Pure ECS实现的,写着发现没法加物理,只能变成Hybrid ECS,实例化GameObject加上Collider,还好更新Collider的位置开销不大,主要是RayCast开销大,用于Agent感知周边车辆位置。但之后Profile发现Job里开销最大的是Raycast物理,就改成了用Job System构造BVH,速度变快很多。

之前:

之后

目前可以在我的笔记本上以30fps跑25000辆车.

代码都挂在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位置。

参考资料

[Smart City] on Data Sources | 智慧城市的数据源, 道路的数据是大翔抓取的,在这篇文章中他描述了获取方法

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

欢迎关注微信公众号:码工图形

发表评论

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