Unity DOTS Life Tutorial entities 0.4.0 preview.10 #2 Worker Threads : updated to entities 0.8.0

Published January 03, 2020 by AntHillPlan
Do you see issues with this article? Let us know.
Advertisement

<<Previous Tutorial

Next Tutorial>>

This tutorial covers changing the jobs from the first tutorial to be scheduled on worker threads.

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 (commits after this tutorial have been made). The Zip file of the tutorial is here.

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.

To change the code to be multithreaded only the systems need changes no changes are needed to the Entities or Components in this case. First I will change the UpdateLiveSystem since it is a very simple change. The line "var job = " was added. The line "}).Run();" was changed to "}).Schedule(inputDeps);" this causes the job to be scheduled on worker threads and causes inputDeps to be returned where the first change assigns it to job.

Finally "return default;" is changed to "return job;" to return the inputDeps+ any dependencies introduced by this job.

public class UpdateLiveSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        float zDead = ECSGrid.zDead;
        float zLive = ECSGrid.zLive;
        
        var job = Entities
            .WithChangeFilter<NextState>()
            .ForEach((ref Live live,ref Translation translation, inNextState nextState) => {
                live.value = nextState.value;
                translation.Value = new float3(translation.Value.x, translation.Value.y,
                math.select( zDead,zLive, live.value == 1));
            }).Schedule(inputDeps);

        return job;
    }
}

The UpdateLiveSystem system was easy to change because it is self contained and references nothing out of the single entity being processed by the foreach. The GenerateNextStateSystem needs more work to change because it needs to access 8 other entities. It only needs to read from them so it is still possible for this job to run multithreaded. Here is the old code.

protected override JobHandle OnUpdate(JobHandle inputDeps)
{
    int[] stay = new int[9];
    int[] born = new int[9];
    stay[2] = stay[3] = 1; // does NOT include self in count
    born[3] = 1;

    var liveLookup = GetComponentDataFromEntity<Live>();

    Entities.WithoutBurst().ForEach((ref NextState nextState, in Live live, in Neighbors neighbors) => {
        int numLiveNeighbors = 0;

        numLiveNeighbors += liveLookup[neighbors.nw].value;
        numLiveNeighbors += liveLookup[neighbors.n].value;
        numLiveNeighbors += liveLookup[neighbors.ne].value;
        numLiveNeighbors += liveLookup[neighbors.w].value;
        numLiveNeighbors += liveLookup[neighbors.e].value;
        numLiveNeighbors += liveLookup[neighbors.sw].value;
        numLiveNeighbors += liveLookup[neighbors.s].value;
        numLiveNeighbors += liveLookup[neighbors.se].value;

        //Note math.Select(falseValue, trueValue, boolSelector)
        nextState.value = math.select( born[numLiveNeighbors],stay[numLiveNeighbors], live.value== 1);
    }).Run();
    
    return default;
}

"var liveLookup = GetComponentDataFromEntity<Live>(); " accesses entities outside the job so needs to be a read only variable. [ReadOnly] is not used on local variables but on fields. So it can't be used inside a ForEach() lambda expression. So instead a struct is needed.

"struct SetLive : IJobForEach<NextState, Live, Neighbors> {" that struct has

"[ReadOnly]public ComponentDataFromEntity<Live> liveLookup; " the readonly field. Then OnUpdate is replaced with this

protected override JobHandle OnUpdate(JobHandle inputDeps) {
    // make a native array of live components indexed by entity
    ComponentDataFromEntity<Live> statuses = GetComponentDataFromEntity<Live>();

    SetLive neighborCounterJob = new SetLive() {
        liveLookup = statuses,
    };
    
    JobHandle jobHandle = neighborCounterJob.Schedule(this, inputDeps);

    return jobHandle;
}

which assigns to livelookup in the SetLive struct initialization, which is allowed for [ReadOnly] fields. Then schedules and returns a jobHandle like UpdateLiveSystem does. Here is the SetLive struct.

struct SetLive : IJobForEach<NextState, Live, Neighbors> {

    [ReadOnly]public ComponentDataFromEntity<Live> liveLookup;

    public void Execute(ref NextState nextState, [ReadOnly] ref Live live,[ReadOnly] refNeighbors neighbors){

        int numLiveNeighbors = 0;

        numLiveNeighbors += liveLookup[neighbors.nw].value;
        numLiveNeighbors += liveLookup[neighbors.n].value;
        numLiveNeighbors += liveLookup[neighbors.ne].value;
        numLiveNeighbors += liveLookup[neighbors.w].value;
        numLiveNeighbors += liveLookup[neighbors.e].value;
        numLiveNeighbors += liveLookup[neighbors.sw].value;
        numLiveNeighbors += liveLookup[neighbors.s].value;
        numLiveNeighbors += liveLookup[neighbors.se].value;

        //Note math.Select(falseValue, trueValue, boolSelector)
        // did not want to pass in arrays so change to
        // 3 selects
        int bornValue = math.select(0, 1, numLiveNeighbors == 3);
        int stayValue = math.select(0, 1, numLiveNeighbors == 2);
        stayValue = math.select(stayValue, 1, numLiveNeighbors == 3);

        nextState.value = math.select( bornValue,stayValue, live.value== 1);
    }
}

The Execute function has a different format for its signature than lambda did before.

"(ref NextState nextState, in Live live, in Neighbors neighbors)" changed to

"(ref NextState nextState, [ReadOnly] ref Live live,[ReadOnly] ref Neighbors neighbors)"

"in" is just short hand for [ReadOnly] ref so these signatures are really the same but the compiler does not recognize this case yet. The other thing that has been changed is

nextState.value = math.select( born[numLiveNeighbors],stay[numLiveNeighbors], live.value== 1);

changed to

int bornValue = math.select(0, 1, numLiveNeighbors == 3);
int stayValue = math.select(0, 1, numLiveNeighbors == 2);
stayValue = math.select(stayValue, 1, numLiveNeighbors == 3);
nextState.value = math.select( bornValue,stayValue, live.value== 1);

job structures cannot have variable length fields in them which is why ComponentDataFromEntity<Live> is a pointer to an array, not the whole array. Instead of setting up born and stay as pointers to arrays I just switched 2 array lookups to 3 select statements I don't know which is faster so I did the one that was faster for me to write.

After all this, the ECS multithreaded code only runs a few percent faster than the GameObject code on a 400x400 grid :( . Why? It turns out that this code is bound by setting up the graphics for 160,000 cells. I used the Hybrid graphics system which still fairly young. I could not find a way to un-enable the renderer of the cells in ECS as I did in the GameObject version. So I just moved them out of the Camera view which still means some graphics code is still calculated even though the result not used. I think the future version of DOTS will add features to handle just turning off a renderer. The other reason the ECS is not faster is that the simulation is very simple and fast. So even though it runs much faster on the ECS it does not make much difference. Something like simulating a complete city with traffic in Cities Skylines is an example of a case where faster simulation would have an effect.

Next Tutorial>> CommandBuffers, Tags [UpdateAfter]

Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement