<<Previous Tutorial Next Tutorial>>
This tutorial covers using CommandBuffers, Tags and WithAll() to reduce the number of entities processed in some steps. Also using [UpdateAfter()] and [UpdateInGroup()] to set the systems updating order.
Build tested on entities 0.4.0 preview.10 . Checked against 2019.3.5f1 & Entities 0.8.0 Preview.8 on March 13, 2020. Packages used are shown here Note: DOTS Platforms is not the latest. it is only 0.2.1 preview 4 because Entities 0.8.0 does not compile with 0.2.2
The source is at https://github.com/ryuuguu/Unity-ECS-Life.git . The Zip file of the project of this tutorial is at http://ryuuguu.com/unity/ECSLifeTutorials/ECSLifeTutorial3.zip.
Warning: DOTS is still bleeding edge tech. Code written now will not be compatible with the first release version of DOTS. There are people using the current version DOTS for release games, but they usually freeze their code to a specific package and will not be able to use future features without rewriting existing code first to compile and run with newer versions. So if you need 10,000 zombies active at once in your game this is probably the only way to do it. Otherwise, this tutorial is a good introduction if you think you will want to simulate thousands of entities in the future or just want to get a feel for code this type of thing in Unity.
The idea behind these changes is that the rendering system (RenderMeshSystemV2) is the bottle neck and not moving meshes unnecessarily will speed things up. There will be a cost to the extra calculations to not move most meshes.
UpdateLiveSystem assigns the nextState to live and then changes the translation of the entity to match the live status. In a production system I would just put "if (nextState != live){make changes}", but this way shows CommandBuffers and Tags. So UpdateLive will be split into two systems. UpdateMarkChangeSystem and UpdateMoveChangedSystem. UpdateMarkChangeSystem will change the live status and add a ChangedTag to the entity. UpdateMoveChangedSystem will move all entities with a changed tag to their correct position and remove the ChangedTag. A Tag is just a component with no fields. Structural changes to entities can not be made inside jobs. They can only be made on the main thread when no other jobs are running. So a job places the change in a command buffer which executes the changes on the main thread when no other jobs are running.
First, the command buffer system must be declared and initialized inside a JobComponentSystem.
protected EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;
protected override void OnCreate() {
base.OnCreate();
// Find the ECB system once and store it for later usage
m_EndSimulationEcbSystem = World
.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
this is added to the top of the new UpdateMarkChangeSystem job systems. Inside the OnUpdate method at the top to "Acquire an ECB and convert it to a concurrent one to be able to use it from a parallel job." (from the manual)
var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
then at the bottom add this so the EndSimulationEntityCommandBufferSystem can run when all jobs using it are finished.
m_EndSimulationEcbSystem.AddJobHandleForProducer(jobHandle);
The UpdateMoveChangedSystem also needs a CommandBuffer but I will use a BeginSimulationEntityCommandBufferSystem and change the running order to PresentationSystemGroup. The code and reasons for these changes is further down in the tutorial.
The signature of the lambda expressions need to add "Entity entity" since we are making changes to the entity, not just components on the entity and "int entityInQueryIndex" entityInQueryIndex is used to schedule the commands in the command buffer so that order is deterministic and stays the same from one run to the next. Also, .WithAll<ChangedTag>() is added to UpdateMoveChangedSystem so that only entities with the ChangeTag are processed.
.ForEach((Entity entity, int entityInQueryIndex, ref Live live, in NextState nextState)=> {
.WithAll<ChangedTag>()
.ForEach((Entity entity, int entityInQueryIndex, ref Translation translation, in Live live) => {
Add or remove the tag inside the jobs
ecb.AddComponent<ChangedTag>(entityInQueryIndex, entity);
ecb.RemoveComponent<ChangedTag>(entityInQueryIndex, entity);
The last change is to set the system update order. The desired order is
- GenerateNextStateSystem
- UpdateMarkChangeSystem
- Execute Add ChangeTags in buffer
- UpdateMoveChangeSystem
- Execute remove ChangeTag in buffer
The SystemGroup order in the documentation
- InitializationSystemGroup
- SimulationSystemGroup
- – BeginSimulationEntityCommandBufferSystem
- – SimulationSystemGroup JobComponentSystem
- – EndSimulationEntityCommandBufferSystem
- PresentationSystemGroup
- – BeginPresentationEntityCommandBufferSystem
- – PresentationSystemGroup JobComponentSystem
- – EndPresentationEntityCommandBufferSystem
Unfortunately EndPresentationEntityCommandBufferSystem does not exist in Entities.0.4.0 so instead I used BeginSimulationEntityCommandBufferSystem. It is possible to add your own Command buffer also. Currently the order is UpdateMarkChangeSystem, GenerateNextStateSystem, UpdateMoveChangedSystem all in SimulationSystemGroup
We want
adding [UpdateAfter()] and [UpdateInGroup()] attributes to the code forces this.
[UpdateInGroup(typeof(PresentationSystemGroup))]
[AlwaysSynchronizeSystem]
[BurstCompile]
public class UpdateMoveChangedSystem : JobComponentSystem {
[UpdateBefore(typeof(UpdateMarkChangeSystem))]
public class GenerateNextStateSystem : JobComponentSystem {
Also if these systems are in the same file, ordering them in the code to match the update order makes the code easier to read. Since Update order is supposed to be deterministic I do not think it can affect update ordering.
The final code for the UpdateMarkChangeSystem
[AlwaysSynchronizeSystem]
[BurstCompile]
public class UpdateMarkChangeSystem : JobComponentSystem {
protected EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;
protected override void OnCreate() {
base.OnCreate();
// Find the ECB system once and store it for later usage
m_EndSimulationEcbSystem = World
.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
JobHandle jobHandle = Entities
.ForEach((Entity entity, int entityInQueryIndex, ref Live live, in NextState nextState)=> {
if (live.value != nextState.value) {
ecb.AddComponent<ChangedTag>(entityInQueryIndex, entity);
}
live.value = nextState.value;
}).Schedule( inputDeps);
m_EndSimulationEcbSystem.AddJobHandleForProducer(jobHandle);
return jobHandle;
}
}
The final code for UpdateMoveChangedSystem
[UpdateInGroup(typeof(PresentationSystemGroup))]
[AlwaysSynchronizeSystem]
[BurstCompile]
public class UpdateMoveChangedSystem : JobComponentSystem {
protected BeginSimulationEntityCommandBufferSystem m_BeginSimulationEcbSystem;
protected override void OnCreate() {
base.OnCreate();
// Find the ECB system once and store it for later usage
m_BeginSimulationEcbSystem = World
.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
float zDead = ECSGrid.zDead;
float zLive = ECSGrid.zLive;
var ecb = m_BeginSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
var jobHandle =
Entities
.WithAll<ChangedTag>()
.ForEach((Entity entity, int entityInQueryIndex, ref Translation translation, in Live live) => {
translation.Value = new float3(translation.Value.x, translation.Value.y,
math.select(zDead, zLive, live.value == 1));
ecb.RemoveComponent<ChangedTag>(entityInQueryIndex, entity);
}).Schedule(inputDeps);
m_BeginSimulationEcbSystem.AddJobHandleForProducer(jobHandle);
return jobHandle;
}
}
In conclusion, CommandBuffers are used to run structural changes as they cannot be made inside jobs and Tags are just minimal components with no fields. The running speeds on the code before and after the changes were not noticeably different. In general, many speed optimizations work by adding a fixed overhead in exchange for faster computations. The Life games computations are so simple and fast that most optimizations will just add complexity without much if any, improvements.