Unity ECS 架构与交通模拟的实现
- 作者:admin
- /
- 时间:2018年11月30日
- /
- 浏览:8106 次
- /
- 分类:厚积薄发
笔者在尝试做交通和行人的模拟,自然就想到了Unity 2018的新功能ECS + Job System,线性数据+多线程提高模拟速度,因此本文分两部分:ECS和交通模拟,以下为一个WIP的成果,感兴趣的朋友可以通过开源库下载:
https://lab.uwa4d.com/lab/5bdf5ee572745c25a8fab19a
该项目的实现主要有几个模块:生成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辆车。
代码都挂在UWA Lab上了,详见OSMTrafficSim:
https://lab.uwa4d.com/lab/5bdf5ee572745c25a8fab19a
下面讲一讲我对于ECS的理解。
一,Unity ECS
ECS是Entity-Component-System的简称,2018推出的新功能。是一种比较新的框架体系,主要优点是:
- 处理大量物体时性能较好,data-layout对Cache友好,便于多线程计算。
- 耦合性低,代码清晰.Component只有数据没有逻辑,System只有逻辑没有状态。
Unity传统框架是有Entity-Component思想的,但不是ECS这个Entity-Component。传统Unity中Gameobject是Entity,MonoBehavior是Component挂在GameObject上。这样数据分散在内存多处,用指针获取,这样Cache-Miss会很多。而且数据耦合严重,线程不安全。
ECS的数据基本都是按一维数组形式存储的,很容易一起放进Cache,加快CPU计算速度。要知道内存-L3-L2-L1的数据读取花费的CPU指令周期是近似数量级降低的。用ID形式访问也对多线程比较友好,避免Data-Race。
笔者更关心计算性能的提升,最明显的是对大量物体的逻辑与渲染有帮助,比如Swarm的模拟,游戏中就可以做万人同屏等了,效率提升可能是数量级的。而对非大量物体的游戏中,提升有多少就不好说了。物理模拟可能是比较有帮助的,可以分配到多线程。其他的逻辑呢,可是本来开销就和物理和渲染不在同一个数量级的。
二,精英的一些基本概念
Unity官方有一些介绍:
https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Documentation/content/ecs_concepts.md
World
Unreal中也有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的实例,注入了数据。
1 EntityManager.CreateArchetype
2 EntityManager.CreateEntity
IComponentData
Component类的接口,所有存数据的地方。注意这是个Struct,其元素必须是非托管的,blitable类型的。
比如bool,int[],NativeArray<>都不能在IComponentData中存在。Unity.Mathematics里面的类都可以。另外每个IComponentData一般会声明一个DataWrapper,这个Wrapper才是一个可以挂在Entity上面的Component。
1 [Serializable]
2 public struct RotationSpeed : IComponentData
3 {
4 public float Value;
5 }
6
7 public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { }
ComponentGroup,Inject,SubtractiveComponent
这几个存在的意义在于:ComponentSystem的Update对于哪些Entity起作用?
https://forum.unity.com/threads/how-the-demo-manages-to-selectively-enable-only-some-system-on-some-scene.531902/#post-3501129
Inject对象声明后会在ComponentSystem的OnCreateManager()之前获取到需要参与计算的Entity。
举个例子,其中SubtractiveComponent代表了,System关心的Entity不能有这些Component。下面这个例子中,group会获取到所有有Position,有Rigidbody,但是没有MeshCollider的Entity。
1class MySystem : ComponentSystem
2{
3 public struct Group
4 {
5 // ComponentDataArray lets us access IComponentData
6 [ReadOnly]
7 public ComponentDataArray<Position> Position;
8
9 // ComponentArray lets us access any of the existing class Component
10 public ComponentArray<Rigidbody> Rigidbodies;
11
12 // Sometimes it is necessary to not only access the components
13 // but also the Entity ID.
14 public EntityArray Entities;
15
16 // The GameObject Array lets us retrieve the game object.
17 // It also constrains the group to only contain GameObject based entities.
18 public GameObjectArray GameObjects;
19
20 // Excludes entities that contain a MeshCollider from the group
21 public SubtractiveComponent<MeshCollider> MeshColliders;
22
23 // The Length can be injected for convenience as well
24 public int Length;
25 }
26 [Inject] private Group m_Group;
27
28
29 protected override void OnUpdate()
30 {
31 // Iterate over all entities matching the declared ComponentGroup required types
32 for (int i = 0; i != m_Group.Length; i++)
33 {
34 m_Group.Rigidbodies[i].position = m_Group.Position[i].Value;
35
36 Entity entity = m_Group.Entities[i];
37 GameObject go = m_Group.GameObjects[i];
38 }
39 }
40}
如果不用Inject,也可以ComponentGroup方法获得要参与计算的Entity。下面这个例子中,找到了所有包含Position, Rigidbody, SharedGrouping组件的Entity,注意ComponentGroup还有filter方法选择一部分Entity。
1struct SharedGrouping : ISharedComponentData
2{
3 public int Group;
4}
5
6class PositionToRigidbodySystem : ComponentSystem
7{
8 ComponentGroup m_Group;
9
10 protected override void OnCreateManager(int capacity)
11 {
12 // GetComponentGroup should always be cached from OnCreateManager, never from OnUpdate
13 // - ComponentGroup allocates GC memory
14 // - Relatively expensive to create
15 // - Component type dependencies of systems need to be declared during OnCreateManager,
16 // in order to allow automatic ordering of systems
17 m_Group = GetComponentGroup(typeof(Position), typeof(Rigidbody), typeof(SharedGrouping));
18 }
19
20 protected override void OnUpdate()
21 {
22 // Only iterate over entities that have the SharedGrouping data set to 1
23 // (This could for example be used as a form of gamecode LOD)
24 m_Group.SetFilter(new SharedGrouping { Group = 1 });
25
26 var positions = m_Group.GetComponentDataArray<Position>();
27 var rigidbodies = m_Group.GetComponentArray<Rigidbody>();
28
29 for (int i = 0; i != positions.Length; i++)
30 rigidbodies[i].position = positions[i].Value;
31
32 // NOTE: GetAllUniqueSharedComponentDatas can be used to find all unique shared components
33 // that are added to entities.
34 // EntityManager.GetAllUniqueSharedComponentDatas(List<T> shared);
35 }
36}
Job
一般System里更新Entity的数据都是Job的方式,就是一个多线程的任务,方便CPU调度。
native方式,所以非托管的方式就跟写C++一样了,比如Job里想要一个定长int数组:
Malloc一下,最后要Free掉
1 int* _array= (int*)UnsafeUtility.Malloc(100 * sizeof(int), sizeof(int), Allocator.Temp);
2 ......
3 ......
4 UnsafeUtility.Free(_array, Allocator.Temp);
5 _array= null;
所以一个ECS的基本逻辑是:
1 rt(){
2 创建Entity
3 System找到感兴趣的Entity
4 }
5
6 Update(){
7 System开Job更新Entity的数据
8 }
三,两个有意思的ECS示例项目
Voxelman,跳舞的体素小人
首先已经有了两个骨骼运动的小人,小人的模型会运行时烘焙成MeshCollider。System首先设置好Voxel,然后RayCast那个小人的collider获得Voxel的位置,最后更新Voxel的位置,渲染。
https://lab.uwa4d.com/lab/5b442a89d7f10a201faf67f3
uSpringBones,飘带物理运算的ECS版本
这个就没有ComponentSystem,全是Monobehavior开Job更新,Component主要用于更新Transform位置。
https://lab.uwa4d.com/lab/5bf67e9d72745c25a836b5ca
参考资料
OSMTrafficSim,笔者项目
https://github.com/maajor/OSMTrafficSim
官方ECS介绍,Github上上这个比官网的全一些
https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Documentation/index.md
Voxelman,有意思的ECS项目
https://lab.uwa4d.com/lab/5b442a89d7f10a201faf67f3
uSpringBones,有意思的ECS项目
https://lab.uwa4d.com/lab/5bf67e9d72745c25a836b5ca
All of the Unity ECS Job System gotchas so far 很多ECS的小Tips!
https://gametorrahod.com/all-of-the-unitys-ecs-job-system-gotchas-so-far-6ca80d82d19f
这是侑虎科技第480篇文章,感谢作者Major供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)
作者主页:http://ma-yidong.com,作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!