Unity DOTS 多线程中的时序问题

环境:
Unity:6000.2.10f1
Entities:1.4.3

参考:
https://github.com/Unity-Technologies/EntityComponentSystemSamples

时序问题

在DOTS中,系统的执行顺序是由系统组(System Group)和系统更新顺序(Update Order)决定的。当多个系统同时访问同一数据时,可能会出现时序问题,导致数据竞争和不一致。

1.系统组和更新顺序

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
InitializationSystemGroup
├─ BeginInitializationEntityCommandBufferSystem
├─ (你的 Initialization systems)
└─ EndInitializationEntityCommandBufferSystem


SimulationSystemGroup
├─ BeginSimulationEntityCommandBufferSystem

├─ FixedStepSimulationSystemGroup
│ ├─ BeginFixedStepSimulationEntityCommandBufferSystem
│ ├─ (Physics / Fixed systems) //物理系统通常会放在FixedStepSimulationSystemGroup中,因为它们需要以固定的时间步长执行,以确保物理模拟的稳定性和一致性。
│ └─ EndFixedStepSimulationEntityCommandBufferSystem

├─ SimulationSystemGroup(普通 Simulation Systems)

├─ LateSimulationSystemGroup
│ ├─ (Late systems)

└─ EndSimulationEntityCommandBufferSystem


PresentationSystemGroup
├─ BeginPresentationEntityCommandBufferSystem
├─ (Rendering / Hybrid Renderer)
└─ EndPresentationEntityCommandBufferSystem

2.数据竞争和不一致

当多个系统同时访问同一数据时,可能会出现数据竞争和不一致的问题。例如,如果一个系统正在修改一个组件的数据,而另一个系统正在读取同一组件的数据,可能会导致读取到不一致的值。为了解决这个问题,可以使用EntityCommandBuffer来延迟对数据的修改,确保数据的访问是安全的。例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class MySystem : SystemBase
{
protected override void OnUpdate()
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
...
ecb.SetComponent(entity, new MyComponent { Value = myComponent.Value });
...
ecb.Playback(EntityManager);
ecb.Dispose();
}
}

上面是自己创建的EntityCommandBuffer,实际开发中通常会使用系统提供的EntityCommandBufferSystem来创建和管理EntityCommandBuffer,以确保性能和安全性。通过合理地组织系统的执行顺序和使用EntityCommandBuffer来管理数据访问,可以有效地解决DOTS中的时序问题,确保游戏逻辑的正确性和性能的优化。

1
2
3
4
var ecb =
SystemAPI
.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter();

上面的EndSimulationEntityCommandBufferSystem是一个系统,提供了一个EntityCommandBuffer。上面的这个ecb,它会在SimulationSystemGroup的末尾执行(LateSimulationSystemGroup之后)。

1
2
3
4
var job = new SpawnJob
{
ECB = ecb,
}.ScheduleParallel(state.Dependency);

这个job不一定能在当前帧执行完成。

1
2
3
4
state.Dependency = new SpawnJob
{
ECB = ecb,
}.ScheduleParallel(state.Dependency);

这样写可以保证job在当前系统的依赖关系上执行,确保在LateSimulation前执行完成。

1
2
3
4
5
6
7
SpawnJob

系统依赖链

LateSimulation

ECB Playback

所以如果需要销毁实体,一般应该放到latesimulationgroup中,修改数据的操作应该放到simulationgroupfixedsimulationgroup,确保ecb的销毁命令最后执行。