Search Unity

Cellular Automata with DOTS

Discussion in 'Entity Component System' started by kingstone426, Jan 5, 2020.

  1. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    I've been toying around with a cellular automata implementation in DOTS but can't find an elegant and fast solution.

    In essence, I would like to run a job that updates the state for each cell depending on the state of its neighbors. My initial approach was to pass a ComponentDataFromEntity<CellComponent> to the job, but of course DOTS won't let me read+write to the same set of components in parallel.

    How could I go about passing the old (previous frame) CellComponent data as an input for my job, while allowing the job to overwrite the new (next frame) CellComponent in parallel? Something like (pseudo code):

    Code (csharp):
    1. struct CellularAutomaton : IJobForEachWithEntity<CellComponent>
    2. {
    3.   [ReadOnly] public ComponentDataFromEntityCLONED<CellComponent> previousFrameData;
    4.  
    5.   public void Execute(Entity entity, CellComponent cellComponent)
    6.   {
    7.      /* update cellComponent using previousFrameData of my neighbors */
    8.   }
    9. }
    I can make it work using EntityQuery.ToComponentDataArray and EntityQuery.CopyFromComponentDataArray but it seems a bit too slow to me.

    Would you choose another approach altogether?
     
  2. LazyGameDevZA

    LazyGameDevZA

    Joined:
    Nov 10, 2016
    Posts:
    143
    What you're describing is a multi-step process even though it can be done in one job it's still important to just understand how this multistep process works. You essentially want to calculate the new value, but not apply it immediately.

    There are multiple approaches to this, but in my view this would be the simplest:

    Have one component: CellComponent

    Using an EntityCommandBuffer you'll then have a job that calculates each cell's new value, but doesn't apply it immediately. When submitting changes to the EntityCommandBuffer the values will only be applied at the sync point the ECB would run.

    If this is still too slow there might be some value building up some structure that enables faster querying of the data in question before you run your job, but that likely gets complex. The above solution will still allow the entirety of your job to run knowing that you won't override data that you'll need for your calculations.
     
    kingstone426 likes this.
  3. paul-figiel

    paul-figiel

    Joined:
    Feb 9, 2017
    Posts:
    9
    I would just add the previous cell state component to your entities.
    And adding [NativeDisableParallelForRestriction] if you only modify the cell you are visiting and checking adjacent cells previous cell state seems reasonable.
    At the end of the frame you can then have a system setting all previous states to current states.
     
  4. LazyGameDevZA

    LazyGameDevZA

    Joined:
    Nov 10, 2016
    Posts:
    143
    My proposed solution would not require write permission on the current entity's cell component. I would assume it's there to serve as the initial value to be modified, not completely overwritten. The use of an EntityCommandBuffer would mean the write operation is delayed until that sync point so there's no need to then require [NativeDisableParallelForRestriction] since all you're doing is reading and not any writing.

    I mentioned that it's a multistep process as the new value for CellComponent can only be written AFTER all entities have their new CellComponent values calculated. It could be possible to buffer these values yourself by writing the value to a different component and setting up another job that'll copy the value back after the main calculation job is finished, but that's essentially what an EntityCommandBuffer achieves already.
     
  5. LazyGameDevZA

    LazyGameDevZA

    Joined:
    Nov 10, 2016
    Posts:
    143
    All that said you might find that having some custom data structure that can take care of the calculations and eventually write out your data as entities with components might be a more optimal approach. It mostly depends on how "alive" this algorithm should feel. If it's being used as level generation there's little need to have all the data that's necessary for the generation process be managed by the entity manager. On the other hand if it's a core part of gameplay having to interact with the data there's more to gain by managing it using entity manager.
     
  6. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    Great feedback! :D

    I agree both suggested approaches could be considered multistep (or "double buffered") processes. Also, both are quite easy to implement which is nice!

    Doing a quick test implementation, I couldn't get the EntityCommandBuffer working with BurstCompile so I think the separate component approach is the better/faster option right now.

    Thanks a lot!
     
  7. LazyGameDevZA

    LazyGameDevZA

    Joined:
    Nov 10, 2016
    Posts:
    143
    @kingstone426 did you use the concurrent version of the ECB in your solution? I suspect if you were using the ECB as is you would've had issues because of an IJobForEach running in parallel.
     
  8. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    Yes I am using EntityCommandBuffer.Concurrent which seems to be working ok except it prevents burst compilation.

    Processing 1 million entities takes around 6ms using the "two component approach" but more than 1200ms (!) using the "ECB approach". Even with burst disabled, the "two component approach" takes only 220ms, so maybe my implementation is completely bonkers.

    Here is the code for the "two component approach":
    (I decided to use a Next-component instead of a Previous-component, but I suppose it's the same. And I love generics!)

    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Jobs;
    5.  
    6. [assembly: RegisterGenericComponentType(typeof(Next<CellComponent>))]
    7.  
    8. public class CellularAutomatonTwoComponents : JobComponentSystem
    9. {
    10.     protected override void OnCreate()
    11.     {
    12.         using (var tempArray = new NativeArray<Entity>(1000000, Allocator.Temp))
    13.         {
    14.             EntityManager.CreateEntity(EntityManager.CreateArchetype(typeof(CellComponent),typeof(Next<CellComponent>)), tempArray);
    15.         }
    16.     }
    17.  
    18.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    19.     {
    20.         var jobHandle = new ProcessForEach() {cells = GetComponentDataFromEntity<CellComponent>()}.Schedule(this, inputDeps);
    21.         jobHandle = new FlipBuffersJob().Schedule(this, jobHandle);
    22.         jobHandle.Complete();
    23.         return jobHandle;
    24.     }
    25. }
    26.  
    27. public struct CellComponent : IComponentData
    28. {
    29.     public float state;
    30. }
    31.  
    32. public struct Next<T> : IComponentData where T : struct, IComponentData
    33. {
    34.     public T value;
    35. }
    36.  
    37. [BurstCompile]
    38. public struct ProcessForEach : IJobForEachWithEntity<Next<CellComponent>>
    39. {
    40.     [ReadOnly] public ComponentDataFromEntity<CellComponent> cells;
    41.  
    42.     public void Execute(Entity entity, int index, ref Next<CellComponent> next)
    43.     {
    44.         next.value.state = cells[entity].state + 1;
    45.     }
    46. }
    47.  
    48. [BurstCompile]
    49. public struct FlipBuffersJob : IJobForEachWithEntity<CellComponent, Next<CellComponent>>
    50. {
    51.     public void Execute(Entity entity, int index, ref CellComponent cell, [ReadOnly] ref Next<CellComponent> next)
    52.     {
    53.         cell = next.value;
    54.     }
    55. }
    And here is the code for the "ECB approach":

    Code (CSharp):
    1. using Unity.Collections;
    2. using Unity.Entities;
    3. using Unity.Jobs;
    4.  
    5. public class CellularAutomatonEntityCommandBuffer : JobComponentSystem
    6. {
    7.     private EntityCommandBufferSystem entityCommandBufferSystem;
    8.  
    9.     protected override void OnCreate()
    10.     {
    11.         // Entities are instantiated in CellularAutomatonTwoComponents
    12.      
    13.         entityCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    14.     }
    15.  
    16.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    17.     {
    18.         var jobHandle = new ProcessForEachCommandBuffer() { cells = GetComponentDataFromEntity<CellComponent>(), commandBuffer = entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent() }.Schedule(this, inputDeps);
    19.         entityCommandBufferSystem.AddJobHandleForProducer(jobHandle);
    20.         jobHandle.Complete();
    21.         return jobHandle;
    22.     }
    23.  
    24.     //[BurstCompile]
    25.     private struct ProcessForEachCommandBuffer : IJobForEachWithEntity<CellComponent>
    26.     {
    27.         [ReadOnly] public ComponentDataFromEntity<CellComponent> cells;
    28.         public EntityCommandBuffer.Concurrent commandBuffer;
    29.  
    30.         public void Execute(Entity entity, int index, [ReadOnly] ref CellComponent cellComponent)
    31.         {
    32.             commandBuffer.SetComponent(index, entity, new CellComponent { state = cells[entity].state + 1 });
    33.         }
    34.     }
    35. }
    Please let me know if something is completely out of wack with my tests! :)
     
  9. Spectralshift

    Spectralshift

    Joined:
    Sep 19, 2018
    Posts:
    8
    Disclaimer: I'm new to DOTS, but I had this problem so I've experimented a bit. However, that was on entites 0.1/0.2, so I'm not sure how it'd look in the new version.


    The problem is highly dependent on your exact cast. For example, if you are doing something like Game of Life, which is a grid, the solution is fundamentally different than if you need to find 'nearest neighbor'.

    My original version, pre-DOTS, was a simple sequential solution that worked over arrays (jagged, multi, or single). Inside Unity, this capped out my frame rate very quickly... maybe 150x150 grid (IIRC).

    My first ECS/DOTS removed arrays and instead created an entity for every grid/array position. Each entity had a grid position (x,y) component and also had 2-4 components that represented the neighbors. I later changed this to 3-8 for bitmasking tests. I also had current state and future state components. Then I had a system that ran for every component that had the neighbor (this eliminated edge cases - eg, 0,0 would have have no "left/down" neighbor entity at -1,0 0,-1 or -1,-1). I had 4-8 systems, depending on the case, looking at each entity that had a particular "neighbor". Each would += a value on the entity being calculated's future state. For example, in the bitmask example, I would have each system (8 of them this time) increment by it's future state (1,2,4,8,16,32,64,128). I'd ensure these completed, then run a system to change the current state to the future state. For example, overwrite the current bitmask by the newly calculated bitmask. In my case, I'd also reset the future state bitmask. As an aside, I ended up doing it my way because I needed to reduce the work on one frame and was willing to process only "one direction" in a frame, something that most cases wouldn't allow. Using dynamic buffers might have worked better otherwise. Using entities was certainly faster (by a factor of ~100) regardless.

    My next, and current, solution was to use persistent native arrays. My actual case is more involved than the Game of Life "alive/dead", but it's similar in nature. I would use two native arrays, with the position in the array corresponding to its 2d grid position. One array holds current state, and the other array contains 'future state'. I use a IJobParallelFor, passing in the two arrays and the width of the grid (future state = read/write, current state = read only, width of grid = read only). Using the index that is passed in, I check on each neighbour and set the future state. In my case, I would then draw the grid according to futurestate (comparing it to current state first - no change means no redraw - as I'm using unity's tilemap), then overwrite the currentstate with futurestate.

    NativeArrays works better because it can be jobified and burst - ECS actually slows things down. That applies while it is a 'simple' grid problem.

    ---

    I tried quite a few things, including ECB, but all of it was vastly inferior to using a multi-step approach. So, for a DOTS implementation, I would start with:

    1) Component with current state (byte ?)
    2) Component with neighbor references (entities)
    3) Component with future state (byte ?)

    Then, forall entites with a current state, pass in (Readonly)current state, (Readonly)Neighbor References, (read/write)Future State. Using IJobForEachWithEntity, you can reference the current state in neighbors via currentState[neighbourEntityReference], and write/increment the future state value (you may have to trick it with writing as futureState[entity] - the current entity index - in many cases).

    Once that completes, you can run a job passing in the current and future state, and just overriding currentstate from futurestate.

    However, the native array approach was easily 10x faster for me. It's identical in principle, except you pass in the arrays instead of components, and have no need for neighbor references (but will have out of bounds safety checks in the job logic).
     
    frankfringe, Zeffi and kingstone426 like this.
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,267
    Besides the fact that you are having a weird issue not getting Burst to work on EntityCommandBuffers, what you have is accurate. EntityCommandBuffers are slower than your extra component approach. In an EntityCommandBuffer, you are doing a copy in parallel to the buffer also writing metadata about the component type, index, and which entity to target. Then the buffer sorts by index and then has to play back each command in a single thread, decoding each command individually. Compare that to the extra component double-buffer solution which boils down to just two parallel memcpy operations.

    If you don't want to have a second component, you can use a NativeContainer instead, which should have equivalent performance. Often when I am dealing with interacting entities like this, I copy the component data into an acceleration structure, and then write results directly to the components.
     
    Sarkahn and kingstone426 like this.
  11. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    I might just have been using an older Burst version that did not support SetComponent. I am working from another machine right now so I can't confirm.

    It seems logical that ECB is slower for the reasons you mention and perhaps an unfortunate drawback for ECBs in general.
     
  12. paul-figiel

    paul-figiel

    Joined:
    Feb 9, 2017
    Posts:
    9
    I agree with DreamingImLatios here, no matter how optimized Command Buffers are, it's always going to be slower to store a billion instructions to process later than to directly process data.

    Also, in a real use case, at some point you're gonna need to access current and previous positions in some systems (maybe for animating), and the data is already there. Whereas with ECB you are hiding this information inside commands.
     
  13. Greenwar

    Greenwar

    Joined:
    Oct 11, 2014
    Posts:
    54
    Mind giving a short demonstration on how you go about writing the results to the components in a performant way? I've always struggled with this part given the current API. :(
     
  14. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    I've come across this type of problem before one approach is to use Native Arrays and limit the reading and writing to separate systems.

    So you would have:

    NativeArray currentValues;
    NativeArray newValues;

    System one takes in readonly currentValues and outputs write only newValues, this lets the system scan around the current cell without hitting boundary issues or overwriting data that will be resampled.

    Then all you need to do is swap newValues and currentValues and repeat, this may need to be done in a system or via a memcpy operation.

    I think the technique is often called double buffering as you use two buffers to prevent overlapping data issues.
     
  15. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    Updated to Unity 2019.30f3 with latest Entities (0.4.0 preview.10) and Burst (1.2.0 preview.11) packages and ECB now works with Burst.

    CellularAutomatonEntityCommandBuffer dropped down to around 9ms, however, that system only populates the ECB. EndSimulationEntityCommandBufferSystem still hogs around 900ms because ECB playback is not parallelized.

    CellularAutomatonTwoComponents is steady at around 5ms and clearly the faster implementation!
     
  16. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,267
    I just use ComponentDataFromEntity. Using an acceleration structure means I am willing to pay the price of random access copies in and out of the structure in order to greatly improve the speed of a more computationally complex algorithm.
     
  17. SisyphusStudio

    SisyphusStudio

    Joined:
    Nov 24, 2018
    Posts:
    9
    @kingstone426 Depending on your implementation, but if you keep your entities in a NativeArray<Entity> and can guarantee that the entities won't change their archetype you can use
    EntityQuery.ToComponentDataArray<T>(Allocator)
    which just copies the chunks components together and then you can access them with the same index as the NativeArray<Entity>.
     
    Sarkahn and kingstone426 like this.
  18. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    Holy smokes! Using EntityQuery.ToComponentDataArray<T>(Allocator), we are now down to 4ms! :D

    Here's the code:

    Code (CSharp):
    1.  
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6.  
    7. public class CellularAutomatonComponentDataArray : JobComponentSystem
    8. {
    9.     private EntityQuery query;
    10.  
    11.     protected override void OnCreate()
    12.     {
    13.         query = EntityManager.CreateEntityQuery(typeof(CellComponent));
    14.      
    15.         using (var tempArray = new NativeArray<Entity>(1000000, Allocator.Temp))
    16.         {
    17.             EntityManager.CreateEntity(EntityManager.CreateArchetype(typeof(CellComponent)), tempArray);
    18.         }
    19.     }
    20.  
    21.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    22.     {
    23.         var current = query.ToComponentDataArray<CellComponent>(Allocator.TempJob);
    24.         var next = new NativeArray<CellComponent>(query.CalculateEntityCount(), Allocator.TempJob);
    25.      
    26.         inputDeps = new ProcessComponentNativeArrays() {current = current, next=next}.Schedule(current.Length, 64);
    27.         inputDeps.Complete();
    28.      
    29.         query.CopyFromComponentDataArray(next);
    30.      
    31.         current.Dispose();
    32.         next.Dispose();
    33.      
    34.         return inputDeps;
    35.     }
    36.  
    37.     [BurstCompile]
    38.     private struct ProcessComponentNativeArrays : IJobParallelFor
    39.     {
    40.         [ReadOnly] public NativeArray<CellComponent> current;
    41.         [WriteOnly] public NativeArray<CellComponent> next;
    42.  
    43.         public void Execute(int index)
    44.         {
    45.             next[index] = new CellComponent {state = current[index].state + 1};
    46.         }
    47.     }
    48. }
    49.  
    Could this be the fastest (non-gpu) implementation possible?
     
    Last edited: Jan 10, 2020
    Arowx likes this.
  19. kingstone426

    kingstone426

    Joined:
    Jun 21, 2013
    Posts:
    44
    Made a simple Game of Life to test the automaton implementation :D



    Here's the CA code:
    using Unity.Jobs;
    using Unity.Mathematics;
    using Unity.Transforms;
    using UnityEngine;
    using Random = UnityEngine.Random;

    public class CellularAutomatonComponentDataArray : JobComponentSystem
    {
    private const int width = 100;
    private const int height = 100;

    private EntityQuery query;

    protected override void OnCreate()
    {
    query = EntityManager.CreateEntityQuery(typeof(CellComponent));

    using (var tempArray = new NativeArray<Entity>(width*height, Allocator.Temp))
    {
    // Allocate entities
    EntityManager.CreateEntity(EntityManager.CreateArchetype(typeof(CellComponent)), tempArray);

    // Setup entity adjacency references
    for (var y=0; y<height; y++)
    {
    for (var x = 0; x < width; x++)
    {
    EntityManager.AddComponentData(tempArray[ToIndex(x,y)], new Translation { Value = new float3(x-width/2, y-height/2, 0) });
    EntityManager.AddComponentData(tempArray[ToIndex(x,y)], new CellComponent() { state = Mathf.RoundToInt(Random.value) });

    }
    }
    }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
    var current = query.ToComponentDataArray<CellComponent>(Allocator.TempJob);
    var next = new NativeArray<CellComponent>(query.CalculateEntityCount(), Allocator.TempJob);
    inputDeps = new ProcessComponentNativeArrays() { current = current, next=next }.Schedule(current.Length, 64);
    inputDeps.Complete();
    query.CopyFromComponentDataArray(next);
    current.Dispose();
    next.Dispose();

    return inputDeps;
    }


    [BurstCompile]
    private struct ProcessComponentNativeArrays : IJobParallelFor
    {
    [ReadOnly] public NativeArray<CellComponent> current;
    [WriteOnly] public NativeArray<CellComponent> next;

    public void Execute(int index)
    {
    var pos = FromIndex(index);

    var total = 0;
    total += current[ToIndex(pos.x - 1, pos.y - 1)].state;
    total += current[ToIndex(pos.x - 0, pos.y - 1)].state;
    total += current[ToIndex(pos.x + 1, pos.y - 1)].state;
    total += current[ToIndex(pos.x - 1, pos.y - 0)].state;
    total += current[ToIndex(pos.x + 1, pos.y - 0)].state;
    total += current[ToIndex(pos.x - 1, pos.y + 1)].state;
    total += current[ToIndex(pos.x - 0, pos.y + 1)].state;
    total += current[ToIndex(pos.x + 1, pos.y + 1)].state;

    if (total==3)
    next[index] = new CellComponent {state = 1};
    else if (total < 2 || total > 3)
    next[index] = new CellComponent {state = 0};
    else
    next[index] = current[index];
    }
    }
    private static int Mod(int value, int modulus) {
    return (value % modulus + modulus) % modulus;
    }

    private static int ToIndex(int x, int y)
    {
    return Mod(x, width) + Mod(y, height) * width;
    }

    private static int2 FromIndex(int i)
    {
    return new int2(i % width, i / width);
    }

    }

    And here is the renderer:
    using Unity.Burst;
    using Unity.Collections;
    using Unity.Entities;
    using Unity.Jobs;
    using Unity.Transforms;
    using UnityEngine;

    namespace Coherence
    {
    [UpdateInGroup(typeof(PresentationSystemGroup))]
    public class CA_TextureRenderer : JobComponentSystem
    {
    private readonly Texture2D texture = new Texture2D(CellularAutomatonComponentDataArray.width, CellularAutomatonComponentDataArray.height, TextureFormat.RGBA32, 0, false);

    private NativeArray<Color32> colors = new NativeArray<Color32>(CellularAutomatonComponentDataArray.width * CellularAutomatonComponentDataArray.height, Allocator.Persistent);

    protected override void OnCreate()
    {
    texture.filterMode = FilterMode.Point;
    Object.FindObjectOfType<SpriteRenderer>().sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
    base.OnCreate();
    }

    protected override void OnDestroy()
    {
    colors.Dispose();
    base.OnDestroy();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
    var colorJob = new ColorJob {colors = this.colors};
    var jobHandle = colorJob.Schedule(this, inputDeps);
    jobHandle.Complete();
    texture.SetPixelData(colorJob.colors, 0);
    texture.Apply();
    return jobHandle;
    }

    [BurstCompile]
    private struct ColorJob : IJobForEach<CellComponent, Translation>
    {
    [NativeDisableParallelForRestriction] public NativeArray<Color32> colors;

    public void Execute(ref CellComponent cell, ref Translation translation)
    {
    colors[CellularAutomatonComponentDataArray.ToIndex((int)(translation.Value.x-CellularAutomatonComponentDataArray.width/2), (int)(translation.Value.y+CellularAutomatonComponentDataArray.height/2))] = new Color32((byte)(cell.state * 255), 0, 0, 255);
    }
    }
    }
    }
     
    Last edited: Jan 12, 2020
    Sarkahn likes this.
  20. Spectralshift

    Spectralshift

    Joined:
    Sep 19, 2018
    Posts:
    8
    Pretty awesome. How many 'tiles' can you process without rendering, roughly? I got up to 2300x2300 (5.29 million) at 13ms with my cobbled together abomination that uses only native arrays. Obviously a different type of solution, but I wanted to see a comparison. What does your CellComponent look like now? I used a tilemap to quickly test it, but that's obviously too slow for anything practical. I'm really curious what the fastest implementation for rendering would be!

    I wanted to optimize it so that I didn't need safety checks, which I'll try later too. My plan is to just start at +1,+1 and end at -1,-1 on the grid, setting the borders' state to 0/dead, and ensuring it doesn't get drawn. Wanted to do it with full safety checks first.

    Oh, as I'm writing this, I was just thought of making it one pixel each and writing it via Texture2d (GetRawTextureData and such). Something else I'll try!


    My cobbled together test:

    Code (CSharp):
    1. using Unity.Jobs;
    2. using Unity.Collections;
    3. using Unity.Burst;
    4.  
    5. [BurstCompile]
    6. public struct SetFutureState : IJobParallelFor
    7. {
    8.     [ReadOnly] public NativeArray<byte> currentState;
    9.     public NativeArray<byte> futureState;
    10.     [ReadOnly] public int width;
    11.     [ReadOnly] public int height;
    12.  
    13.  
    14.     public void Execute(int jobindex)
    15.     {
    16.         // safety checks;
    17.         bool _left = (jobindex) % width != 0;
    18.         bool _up = jobindex < (width * (height - 1));
    19.         bool _right = (jobindex + 1) % width != 0;
    20.         bool _down = jobindex >= width;
    21.  
    22.         int _stateCount = 0;
    23.  
    24.         if (_left)
    25.         {
    26.             _stateCount += currentState[jobindex - 1];
    27.             if (_down)  _stateCount += currentState[jobindex - width - 1];
    28.             if (_up)  _stateCount += currentState[jobindex - 1 + width];
    29.         }
    30.  
    31.         if (_right)
    32.         {
    33.             _stateCount += currentState[jobindex + 1];
    34.             if (_up)  _stateCount += currentState[jobindex + width + 1];
    35.             if (_down)  _stateCount += currentState[jobindex + 1 - width];
    36.         }
    37.    
    38.         if (_up) _stateCount += currentState[jobindex + width];
    39.         if (_down) _stateCount += currentState[jobindex - width];
    40.  
    41.         if (currentState[jobindex] == 0)//is dead
    42.         {
    43.             if (_stateCount == 3) futureState[jobindex] = 1; //make alive
    44.         }
    45.         else
    46.         {
    47.             if (_stateCount == 2 || _stateCount == 3) futureState[jobindex] = 1; //make alive
    48.             else futureState[jobindex] = 0; //make dead
    49.         }
    50.     }
    51. }
    52.  

    And a normal monobehavior game controller. Could be converted, but... simple and easy to see what is happening.

    Code (CSharp):
    1.  
    2. using Unity.Jobs;
    3. using Unity.Mathematics;
    4. using UnityEngine;
    5. using Random = UnityEngine.Random;
    6. using Unity.Entities;
    7. using Unity.Collections;
    8. using UnityEngine.Tilemaps;
    9.  
    10. public class GameController : MonoBehaviour
    11. {
    12.     #region Serialized Fields for Controller
    13.     [SerializeField] private int width = 100;
    14.     [SerializeField] private int height = 100;
    15.  
    16.     [SerializeField] private Tile aliveTile;
    17.     [SerializeField] private Tile deadTile;
    18.  
    19.     [SerializeField] private Tilemap GOLTileMap;
    20.  
    21.     [SerializeField] private bool render = true;
    22.     [SerializeField] private bool useTimer = false;
    23.     [SerializeField] private float updateInterval = 1;
    24.     #endregion
    25.  
    26.     private float updateCounter = 0;
    27.  
    28.     // The only real data the program needs
    29.     public NativeArray<byte> currentState;
    30.     public NativeArray<byte> futureState;
    31.  
    32.     // Avoid GC. Won't work if we try to compare current/future states to avoid excessive draws.
    33.     // Really though, tilemap is super slow, only here to test.
    34.     Vector3Int[] positions;
    35.     Tile[] tileArray;
    36.  
    37.     void Awake()
    38.     {
    39.         currentState = new NativeArray<byte>(width * height, Allocator.Persistent);
    40.         futureState = new NativeArray<byte>(width * height, Allocator.Persistent);
    41.  
    42.         positions = new Vector3Int[futureState.Length];
    43.         tileArray = new Tile[positions.Length];
    44.      
    45.         for (int i = 0; i < currentState.Length; i++) { currentState[i] = (byte)Random.Range(0, 2); }
    46.     }
    47.  
    48.     private void Start()
    49.     {
    50.         UpdateFutureState();
    51.         if (render)
    52.         {
    53.             GOLTileMap.gameObject.SetActive(true);
    54.             DrawTileMap(); }
    55.         else
    56.         {
    57.             GOLTileMap.gameObject.SetActive(false);
    58.         };
    59.     }
    60.  
    61.     void Update()
    62.     {
    63.         if (useTimer)
    64.         {
    65.             updateCounter += Time.deltaTime;
    66.             if (updateCounter >= updateInterval)
    67.             {
    68.                 updateCounter -= updateInterval;
    69.                 UpdateFutureState();
    70.                 if (render) DrawTileMap();
    71.                 currentState.CopyFrom(futureState);// update the current state by overwriting from future state
    72.             }
    73.         }
    74.         else
    75.         {
    76.             UpdateFutureState();
    77.             if (render) DrawTileMap();
    78.             currentState.CopyFrom(futureState);// update the current state by overwriting from future state
    79.         }
    80.     }
    81.      
    82.     private void DrawTileMap()
    83.     {
    84.         for (int i = 0; i < positions.Length; i++)
    85.         {
    86.             positions[i] = Arrayto3dGrid(i);
    87.             if (futureState[i] == 0)
    88.             {
    89.                 tileArray[i] = deadTile;
    90.             }
    91.             else
    92.             {
    93.                 tileArray[i] = aliveTile;
    94.             }
    95.         }
    96.  
    97.         GOLTileMap.SetTiles(positions, tileArray);
    98.     }
    99.  
    100.     private void UpdateFutureState()
    101.     {
    102.         // create the job to read current state of neighbours and set the future state of each object
    103.         JobHandle SetFutureStateJH = new SetFutureState
    104.         {
    105.             currentState = currentState,
    106.             futureState = futureState,
    107.             width = width,
    108.             height = height
    109.         }.Schedule(currentState.Length, 64);
    110.  
    111.         // ensure the job is complete
    112.         SetFutureStateJH.Complete();
    113.     }
    114.  
    115.     private Vector3Int Arrayto3dGrid(int arrayPosition)
    116.     {
    117.         return new Vector3Int(arrayPosition % width, (int)math.floor(arrayPosition / width), 0);
    118.     }
    119.  
    120.     private void OnDestroy()
    121.     {
    122.         currentState.Dispose();
    123.         futureState.Dispose();
    124.     }
    125. }
    126.  
    127.  
    128.  


    Edit:

    I changed it to use texture2d.GetRawTextureData() and write to it in another IJobParallelFor. Got it up to full screen - 1920x1080, so ~2 million cells, operating at ~165 iterations/frames per second. Theoretically, probably could just barely do 4k at 60 frames.

    Image:
     
    Last edited: Jan 13, 2020
    toske_, MNNoxMortem and kingstone426 like this.