尝试做交通和行人的模拟,自然就想到了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
-
处理大量物体时性能较好,data-layout对cache友好,便于多线程计算。
-
耦合性低,代码清晰。Component只有数据没有逻辑,System只有逻辑没有状态。
Unity传统框架是有Entity-Component思想的,但不是ECS这个Entity-Component。传统Unity中Gameobject是Entity,Monobehaivor是component挂在Gameobject上。这样数据分散在内存多处,用指针获取,这样Cache-Miss会很多。而且数据耦合严重,线程不安全。
ECS的一些基本概念
Unity官方有一些介绍
World
ComponentSystem
Entity, EntityManager
1 2 |
EntityManager.CreateArchetype EntityManager.CreateEntity |
IComponentData
1 2 3 4 5 6 7 |
[Serializable] public struct RotationSpeed : IComponentData { public float Value; } public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { } |
ComponentGroup, Inject, SubtractiveComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class MySystem : ComponentSystem { public struct Group { // ComponentDataArray lets us access IComponentData [ReadOnly] public ComponentDataArray<Position> Position; // ComponentArray lets us access any of the existing class Component public ComponentArray<Rigidbody> Rigidbodies; // Sometimes it is necessary to not only access the components // but also the Entity ID. public EntityArray Entities; // The GameObject Array lets us retrieve the game object. // It also constrains the group to only contain GameObject based entities. public GameObjectArray GameObjects; // Excludes entities that contain a MeshCollider from the group public SubtractiveComponent<MeshCollider> MeshColliders; // The Length can be injected for convenience as well public int Length; } [Inject] private Group m_Group; protected override void OnUpdate() { // Iterate over all entities matching the declared ComponentGroup required types for (int i = 0; i != m_Group.Length; i++) { m_Group.Rigidbodies[i].position = m_Group.Position[i].Value; Entity entity = m_Group.Entities[i]; GameObject go = m_Group.GameObjects[i]; } } } |
不用Inject的话,也可以ComponentGroup方法获得要参与计算的Entity。下面这个例子中,找到了所有包含Position, Rigidbody, SharedGrouping组件的Entity,注意ComponentGroup还有filter方法选择一部分Entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
struct SharedGrouping : ISharedComponentData { public int Group; } class PositionToRigidbodySystem : ComponentSystem { ComponentGroup m_Group; protected override void OnCreateManager(int capacity) { // GetComponentGroup should always be cached from OnCreateManager, never from OnUpdate // - ComponentGroup allocates GC memory // - Relatively expensive to create // - Component type dependencies of systems need to be declared during OnCreateManager, // in order to allow automatic ordering of systems m_Group = GetComponentGroup(typeof(Position), typeof(Rigidbody), typeof(SharedGrouping)); } protected override void OnUpdate() { // Only iterate over entities that have the SharedGrouping data set to 1 // (This could for example be used as a form of gamecode LOD) m_Group.SetFilter(new SharedGrouping { Group = 1 }); var positions = m_Group.GetComponentDataArray<Position>(); var rigidbodies = m_Group.GetComponentArray<Rigidbody>(); for (int i = 0; i != positions.Length; i++) rigidbodies[i].position = positions[i].Value; // NOTE: GetAllUniqueSharedComponentDatas can be used to find all unique shared components // that are added to entities. // EntityManager.GetAllUniqueSharedComponentDatas(List<T> shared); } } |
Job
1 2 3 4 5 |
int* _array= (int*)UnsafeUtility.Malloc(100 * sizeof(int), sizeof(int), Allocator.Temp); ...... ...... UnsafeUtility.Free(_array, Allocator.Temp); _array= null; |
1 2 3 4 5 6 7 8 |
Start(){ 创建Entity System找到感兴趣的Entity } Update(){ System开Job更新Entity的数据 } |
两个有意思的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
