Unity的ECS与JobSystem初探

大概学习一下。

传统方式:

image-20211010142008940

问题:(1)数据分散

image-20211010142128499

stuff过于混乱

(2)存在冗余数据

当move的时候,只需要speed,position和rotation,但transform含有lot of stuff

image-20211010142624757

(3)串行方式

所以带来了JobSystem

  • CPU利用率提高,一般游戏都是2core
  • 传统多线程的问题:race condition, context switching
  • thread safe

ECS需要改变思考方式。

image-20211010143126546

Entity:不是game object,只是reference to data,purely date

System:functionality, 关键是一个Filter

例如,一个Render System有一个Render和Position的Filter,那么就会找到所有包含Render和Position的data component构成的Entity

ECS的优势:

  • 不容易写出表现差的代码,原生带有良好的性能
  • 可重用性高
  • 能更充分的利用CPU
  • achetypes在内存中紧密打包,访问速度提高

下面的一个job:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ComputeJobOptimization]
public struct MovementJob : IJobParallelForTransform {
public float moveSpeed;
public float topBound;
public float bottomBound;
public float deltaTime;
// index: id, TransformAccess包含Transform的元数据,除去引用数据
public Execute(int index, TransformAccess transform) {
// 逻辑差距不打
Vector3 pos = transform.position;
pos += moveSpeed * deltaTime * (transform.rotation * new Vector3(0,0,1));
if (pos.z < bottomBound) pos.z = topBound;
transform.position = pos;
}
}

GameManager:

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
41
public class GameManager : MonoBehavior {
#region GAME_MANAGER_STUFF
//...
#endregion
TransformAccessArray transforms;
MovementJob moveJob;
JobHandle moveHandle;

void OnDisable() {
moveHandle.Complete();
transforms.Dispose(); // 手动GC
}

void Update() {
moveHandle.Complete(); //等待完成
moveJob = new MovementJob() { // 创建信息
moveSpeed = enemySpeed,
topBound = topBound,
bottomBound = bottomBound,
deltaTime = Time.deltaTime
};
moveHandle = moveJob.Schedule(transforms); // 传入参数,传递给handle
JobHandle.ScheduleBatchedJobs(); // 执行任务
}

void AddShips(int amount) {
moveHandle.Complete();
transform.capacity = transform.length + amount;
for (int i = 0; i < amount; ++i) {
float xV = Random.Range(leftBound, rightBound);
float zV = Random.Range(0f, 10f);
Vector3 pos = new Vector3(xVal, 0f, zV + topBound);
Quanternion rot = Quanternion.Euler(0f, 180f, 0f);
var obj = Instantiate<GameObject>(prefab, pos, rat);
transforms.Add(obj.transform);
}
count += amount;
fps.SetElementCount(count);
}

}

这种做法基本保持了原有的Unity方案。但我们希望用完整的ECS策略,还需要把Entity分出来。

1
2
3
4
[Serializable] public struct MoveSpeed : IComponentData {
public float Value;
}
public class MoveSpeedComponent : ComponentDataWrapper<MoveSpeed> {}

image-20211010152129930

接下来创建一个System来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MovementSystem : JobComponentSystem {
[ComputeJobOptimization]
public struct MovementJob : IJobParallelForTransform<Position, Rotation, MoveSpeed> { // data inject
public float topBound;
public float bottomBound;
public float deltaTime;
public Execute(ref Position position, [ReadOnly] ref Rotation rotation, [ReadOnly] ref MoveSpeed speed) {
// 逻辑差距不大
float3 pos = position.Value;
pos += speed.Value * deltaTime * math.forward(rotation.Value);
if (pos.z < bottomBound) pos.z = topBound;
position.Value = pos;
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
MovementJob moveJob = new MovementJob {
topBound = GameManager.GM.topBound,
bottomBound = GameManager.GM.bottomBound,
deltaTime = Time.deltaTime
};
JobHandle moveHandle = moveJob.Schedule(this, 64, inputDeps);
return moveHandle;
}
}

在GameManager中:

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
public class GameManager : MonoBehavior {
#region GAME_MANAGER_STUFF
//...
#endregion
EntityManager manager;

void OnDisable() {
moveHandle.Complete();
transforms.Dispose(); // 手动GC
}

void Start() {
fps = GetComponent<FPS>();
manager = World.Active.GetOrCreateManager<EntityManager>();
AddShips(enemyShitCount);
}

void Update() {
if (Input.GetKeyDown("space"))
AddShips(enemyShipIncrement);
}

void AddShips(int amount) {
NativeArray<Entity> entities = new NativeArray<Entity>(amount, Allocator.Temp); // new type collections
manager.Instantiate(enemyShipPrefab, entities); // create entities with component data
for (int i = 0; i < amount; ++i) {
float xV = Random.Range(leftBound, rightBound);
float zV = Random.Range(0f, 10f);
manager.SetComponentData(entities[i], new Position {Value = new float3(xVal, 0f, topBound + zVal)});
manager.SetComponentData(entities[i], new Rotation { Value = new quanternion(0,1,0,0) });
manager.SetComponentData(entities[i], new MoveSpeed { Value = enemySpeed });
}

entities.Dispose(); // GC
count += amount;
fps.SetElementCount(count);
}

}

如果使用burst compiler,很多运算会交给GPU去进行,运算可以进一步优化。

Author

LittleRewriter

Posted on

2021-10-14

Updated on

2021-12-02

Licensed under

Comments