Conway’s Game of Life is a simple simulation of cells’ life-cycle. It consists of a plane made of many squares that all represent a cell, which is either alive or dead. The rules for survival are as follows:

2 or 3 alive neighboring cells lets a cell survive to the next generation

Exactly 3 alive neighboring cells will revive a dead cell

In all other cases, a cell will be dead

So, in this simulation, a cell can be represented by a boolean value which represents either a dead or an alive cell as seen below.

public struct CellComponent : IComponentData { public bool IsAlive ; }

When the cell has been defined, it is time to spawn it into the world. This will be done by a ComponentSystem called SpawnSystem. This system will need an EntityArchetype which represents the Cell inside the simulation. It will also need a Material for a dead cell and a Mesh. Furthermore a static int2 is used to inform the other systems of the size of the world.

public class SpawnSystem : ComponentSystem { public static int2 WorldSize ; private EntityArchetype cellArchetype ; private Material deadMaterial ; private Mesh planeMesh ; }

These variables have to be instatiated and this is done in the OnStartRunning method.

protected override void OnStartRunning ( ) { base . OnStartRunning ( ) ; cellArchetype = World . DefaultGameObjectInjectionWorld . EntityManager . CreateArchetype ( typeof ( LocalToWorld ) , typeof ( RenderMesh ) , typeof ( Scale ) , typeof ( Translation ) , typeof ( CellComponent ) ) ; deadMaterial = new Material ( Shader . Find ( "Standard" ) ) { color = Color . black } ; var p = GameObject . CreatePrimitive ( PrimitiveType . Plane ) ; planeMesh = p . GetComponent < MeshFilter > ( ) . mesh ; Object . Destroy ( p ) ; }

First the EntityArchType is created with a LocalToWorld, RendereMesh and a Scale this is need for drawing the cell. The Translation stores the position of the cell and CellComponent stores the state of the cell.

The Material is created as a standard shader and the color set to black.

The Mesh is a plane taken from a GameObject created using the CreatePrimitve method, which creates a GameObject with a Plane Mesh. This plane can then be copied and the GameObject destroyed.

To create the world the width and height of the world should be defined. To make it easy to change in the Unity editor this will be defined using an IComponentData called WorldSpawnSize.

[ GenerateAuthoringComponent ] public struct WorldSpawnSize : IComponentData { public int Width ; public int Height ; }

This component is flagged with [GenerateAuthoringComponent] which means that it can be added to a GameObject in the editor which has the ConvertToEntity scrips attached. And can, therefore, be changed in the editor.

This component is used in the OnUpdate method of the SpawnSystem to generate the world.

protected override void OnUpdate ( ) { Entities . ForEach ( ( Entity entity , ref WorldSpawnSize worldSize ) = > { WorldSize = new int2 ( worldSize . Width , worldSize . Height ) ; for ( int x = 0 ; x < worldSize . Width ; x ++ ) { for ( int y = 0 ; y < worldSize . Height ; y ++ ) { var c = PostUpdateCommands . CreateEntity ( cellArchetype ) ; PostUpdateCommands . SetComponent ( c , new LocalToWorld ( ) ) ; PostUpdateCommands . SetSharedComponent ( c , new RenderMesh ( ) { material = deadMaterial , mesh = planeMesh } ) ; PostUpdateCommands . SetComponent ( c , new Translation ( ) { Value = new float3 ( x , 0 , y ) } ) ; PostUpdateCommands . SetComponent ( c , new CellComponent ( ) { } ) ; PostUpdateCommands . SetComponent ( c , new Scale ( ) { Value = 0.1f } ) ; } } PostUpdateCommands . DestroyEntity ( entity ) ; } ) ; }

Using Entities.ForEach the system can iterate over all entities with the correct set of components which in this case is WorldSpawnSize. This system will only run a single time at startup. When it runs it will set the static varaible WorldSize to match the information in the compoent. Afterwards, two forloops will go through and create each cell in a column by column pattern.

Inside the inner forloop a new cell is created using the PostUpdateCommands CommandBuffer. This commandbuffer is used to create and edit entities because this can not be done inside a job running on other threads. The buffer schedules the changes and executes them after the Update phase.

The scale is set to 0.1f so the cell will take up exactly a one by one space in the editor. And at last, the entity with the WorldSpawnSize components is destroyed, because it is no longer needed.

Updating cells

When all the cells have been instantiated it is time to make the update-loop which will change the state of each cell as described by the rules of the simulation. To achieve this a new system is created called ChangeCellStateSystem. This system will be a JobComponentSystem which will need a tickRate, an isRunning and a timePassed variable. Without this, the system could be updated 100 times a second, which would be inconvenient.

public class ChangeCellStateSystem : JobComponentSystem { public static float TickRate = 1f ; private bool isRunning ; private float timePassed = 0f ; protected override JobHandle OnUpdate ( JobHandle inputDeps ) { } }

The JobComponentsSystem needs the OnUpdate method. This method will update the local variables then check if the the simulation should update and if so update the cells.

protected override JobHandle OnUpdate ( JobHandle inputDeps ) { UpdateSimVariables ( ) ; if ( ShouldUpdate ( ) ) return UpdateCells ( inputDeps ) ; return inputDeps ; }

To change the tickRate + and - is used and the spacebar changes isStarted as seen in the UpdateSimVariables method below.

private void UpdateSimVariables ( ) { if ( Input . GetKeyDown ( KeyCode . KeypadPlus ) || Input . GetKeyDown ( KeyCode . Equals ) ) { if ( TickRate > 2 ) TickRate + = 1 ; else TickRate - = 0.1f ; if ( TickRate < 0.1f ) TickRate = 0.1f ; } else if ( Input . GetKeyDown ( KeyCode . Minus ) || Input . GetKeyDown ( KeyCode . KeypadMinus ) ) { if ( TickRate >= 2 ) TickRate + = 1 ; else TickRate + = 0.1f ; } if ( Input . GetKeyDown ( KeyCode . Space ) ) isRunning = ! isRunning ; }

The cells will be updated when the simulation is started and the corect amount of time has passed as seen in ShouldUpdated

private bool ShouldUpdate ( ) { if ( ! isRunning ) return false ; timePassed + = UnityEngine . Time . deltaTime ; if ( timePassed >= TickRate ) timePassed = 0f ; else return false ; return true ; }

Updating the cells is done in the UpdateCells method. This method returns a JobHandle which the OnUpdate method needs to return.

Because there is a lot of cells that will need to be updated it will be handled by a job that can be parallelized on multiple threats. This job will need a copy of the state of all other cells. Because to update a cell the number of alive cells is needed and this can only be done by looking at other cells.

Therefore, an ordered copy of the cells states is needed and this is what UpdateCells first creates.

private JobHandle UpdateCells ( JobHandle inputDeps ) { var otherCells = GetEntityQuery ( ComponentType . ReadOnly < CellComponent > ( ) , ComponentType . ReadOnly < Translation > ( ) ) ; var cells = otherCells . ToComponentDataArray < CellComponent > ( Allocator . Persistent ) ; var pos = otherCells . ToComponentDataArray < Translation > ( Allocator . Persistent ) ; var sortOtherCellsJob = new SortOtherCellsJob ( ) { Cells = cells , Positions = pos , N = cells . Length , Height = SpawnSystem . WorldSize . y } . Schedule ( inputDeps ) ; sortOtherCellsJob . Complete ( ) ; }

First all entities with a CellComponent and a Translastion is found using GetEntityQuery where both of them is flaged with ReadOnly. These two components is then put into two nativearrays one for the CellComponents and one for the Translations. These nativearrays are allocated with Allocator.Persistent so they need to be dispose of when they are not in use anymore.

Afterward, the job SortOtherCellsJob is scheduled to sort the arrays, so an individual CellComponent is easier to find. To do this the two nativearrays and the height of the world are needed. The Translations is used to compare two components.

Before the method can move on to updating the CellComponents the sorting job needs to be finished. This is guaranteed when calling the sortOtherCellsJob.Complete method.

private JobHandle UpdateCells ( JobHandle inputDeps ) { . . . var cellStateJob = new CellStateJob ( ) { OtherCells = cells , Width = SpawnSystem . WorldSize . x , Height = SpawnSystem . WorldSize . y } . Schedule ( this , sortOtherCellsJob ) ; cellStateJob . Complete ( ) ; pos . Dispose ( ) ; cells . Dispose ( ) ; return cellStateJob ; }

The UpdatStateJob needs the sorted nativearray of CellComponents and the size of the world. When scheduling this job, the jobhandle from the SortOtherCellsJob should be passed in instead of inputDeps.

In the end, cellStateJob.Complete is called to make sure all the nativearrays is not in use, so they can be disposed of and the JobHandle of the CellStateJob returned.

CellStateJob

The CellStateJob needs a nativearray of ordered CellComponents and the size of the world.

private struct CellStateJob : IJobForEach < CellComponent , Translation > { [ ReadOnly ] public NativeArray < CellComponent > OtherCells ; public int Width ; public int Height ; }

OtherCells are flagged with [ReadOnly] because this job will only read from this nativearray. Furthermore, it makes sure other jobs can access the nativearray at the same time if they also just read from it, which is the case for all the other instances of this job on other threats.

The job is marked with IJobForEach which enables this job to be parallelized. This job iterates over all entities with a CellComponent and a Translation component. The Translation is used to count the number of alive neighbors in the Execute method.

public void Execute ( ref CellComponent cell , ref Translation pos ) { int aliveNeighbours = 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x , pos . Value . z + 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x + 1 , pos . Value . z + 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x + 1 , pos . Value . z ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x + 1 , pos . Value . z - 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x , pos . Value . z - 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x - 1 , pos . Value . z - 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x - 1 , pos . Value . z ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x - 1 , pos . Value . z + 1 ) ? 1 : 0 ; }

The GetStateOfCell returns the state of the cell at a given position. First, it tiles the position, meaning that if the given position exceeds the world size it returns the position from the other side of the world. For example, if the world width is 100 and the x position given is 100 and therefore exceeds it, the x position is set to 0.

The 2D indexing is converted to a 1D index with the formula z + x ⋅ H e i g h t z + x \cdot Height z+x⋅Height. This formula is also used when sorting the CellComponents.

private bool GetStateOfCell ( float x , float z ) { if ( x < 0 ) x = Width - 1 ; else if ( x >= Width ) x = 0 ; if ( z < 0 ) z = Height - 1 ; else if ( z >= Height ) z = 0 ; var c = OtherCells [ ( int ) ( z + x * Height ) ] ; return c . IsAlive ; }

After the number of alive neighbors has been counted the state of the cell is changed.

public void Execute ( ref CellComponent cell , ref Translation pos ) { . . . if ( cell . IsAlive ) cell . ChangeTo = aliveNeighbours == 2 || aliveNeighbours == 3 ; else cell . ChangeTo = aliveNeighbours == 3 ; }

When changing the state of the cells it is not the IsAlive variable but a new ChangeTo variable which is changed. This is done to simplify changing the color of the cells. So the CellComponent now looks like this:

public struct CellComponent : IComponentData { public bool IsAlive ; public bool ChangeTo ; }

SortOtherCellsJob

The sorting method used in this guide is a modified version of MergeSort that does not use recursion and which can use one array to compare to indices and another which is the one being sorted. The implementation found and modified was here:includehelp.

To not make this guide too long the code will not be described but it can be seen in the All Code section.

When the cells have been updated some cells might have turned from dead to alive or the other way around. This means that their color should change from black (dead) to white (alive). This is handled by the ChangeCellColorSystem which changes the shared RendereMesh component. To do this it needs to know what material is used for the dead and the alive cell and what type of mesh to render. This system should update after the cells have changed state which is why it is flagged with [UpdateAfter(typeof(ChangeCellStateSystem))].

[ UpdateAfter ( typeof ( ChangeCellStateSystem ) ) ] public class ChangeCellColorSystem : JobComponentSystem { private Material deadMaterial ; private Material aliveMaterial ; private Mesh planeMesh ; }

These variables is initialiezed in the OnStartRunning mehtod. And is done in the same way as in SpawnSystem.

protected override void OnStartRunning ( ) { base . OnStartRunning ( ) ; deadMaterial = new Material ( Shader . Find ( "Standard" ) ) { color = Color . black } ; aliveMaterial = new Material ( Shader . Find ( "Standard" ) ) { color = Color . white } ; var p = GameObject . CreatePrimitive ( PrimitiveType . Plane ) ; planeMesh = p . GetComponent < MeshFilter > ( ) . mesh ; Object . Destroy ( p ) ; }

ChangeCellColorSystem is a ComponentSystem because this system does not do much except for removing and adding compoents which needs to be done on the main thread. Therefore this system needs an OnUpdate method which uses Entities.ForEach to itertate over all entitites. Furthermore the Entities.ForEach needs to be converted to Entities.WithStructualChanges.ForEach because it changes the structure of the entities.

protected override JobHandle OnUpdate ( JobHandle inputDeps ) { var manager = World . DefaultGameObjectInjectionWorld . EntityManager ; Entities . WithStructuralChanges ( ) . ForEach ( ( Entity entity , ref CellComponent cell ) = > { if ( cell . IsAlive == cell . ChangeTo ) return ; cell . IsAlive = cell . ChangeTo ; manager . RemoveComponent ( entity , typeof ( RenderMesh ) ) ; manager . AddSharedComponentData ( entity , new RenderMesh ( ) { material = cell . IsAlive ? aliveMaterial : deadMaterial , mesh = planeMesh } ) ; } ) . Run ( ) ; return inputDeps ; }

Outside the Entities.ForEach the current EntityManager is cached, for use inside the Entities.ForEach. If the cell has not changed it returns and nothing happens. Otherwise the state is changed and the RenderMesh is changed to the correct one.

Change Cell State Manually

Changing the state of the cells manually enables a player to “paint” the world with alive cells and therefore make interresting simulations. This functionality is enabled by the InputSystem. This system flips the state of the cell underneath the mousepointer. To do this it needs a reffernce to the Main Camera which is cached in the OnStartRunning Method.

public class InputSystem : JobComponentSystem { private Camera cam ; protected override void OnStartRunning ( ) { base . OnStartRunning ( ) ; cam = Camera . main ; } }

The OnUpdate method first checks if the left mousebutton is pressed. If so it schedules a new job to flip the state of the cell at the mouse position. The mouse position is attained by converting screenpoints to worldpoints.

protected override JobHandle OnUpdate ( JobHandle inputDeps ) { if ( ! Input . GetMouseButtonDown ( 0 ) ) return inputDeps ; return new ChangeCellState ( ) { Pos = cam . ScreenToWorldPoint ( Input . mousePosition ) } . Schedule ( this , inputDeps ) ; }

The scheduled job is ChangeCellState and takes the position of the cell to change. It then iterates over all entities with a CellComponent and a Translation and checks if the Translation is the same as the mouseposition. If this is the case the cell’s state is fliped.

struct ChangeCellState : IJobForEach < CellComponent , Translation > { public float3 Pos ; public void Execute ( ref CellComponent cell , ref Translation pos ) { if ( ( int ) ( Pos . x + 0.5f ) == ( int ) pos . Value . x && ( int ) ( Pos . z + 0.5f ) == ( int ) pos . Value . z ) cell . ChangeTo = ! cell . IsAlive ; } } }

All Code

The whole project is avalible on GitHub.

Components

public struct CellComponent : IComponentData { public bool IsAlive ; public bool ChangeTo ; } [ GenerateAuthoringComponent ] public struct WorldSpawnSize : IComponentData { public int Width ; public int Height ; }

SpawnSystem

public class SpawnSystem : ComponentSystem { public static int2 WorldSize ; private EntityArchetype cellArchetype ; private Material deadMaterial ; private Mesh planeMesh ; protected override void OnStartRunning ( ) { base . OnStartRunning ( ) ; cellArchetype = World . DefaultGameObjectInjectionWorld . EntityManager . CreateArchetype ( typeof ( LocalToWorld ) , typeof ( RenderMesh ) , typeof ( Scale ) , typeof ( Translation ) , typeof ( CellComponent ) ) ; deadMaterial = new Material ( Shader . Find ( "Standard" ) ) { color = Color . black } ; var p = GameObject . CreatePrimitive ( PrimitiveType . Plane ) ; planeMesh = p . GetComponent < MeshFilter > ( ) . mesh ; Object . Destroy ( p ) ; } protected override void OnUpdate ( ) { Entities . ForEach ( ( Entity entity , ref WorldSpawnSize worldSize ) = > { WorldSize = new int2 ( worldSize . Width , worldSize . Height ) ; for ( int x = 0 ; x < worldSize . Width ; x ++ ) { for ( int y = 0 ; y < worldSize . Height ; y ++ ) { var c = PostUpdateCommands . CreateEntity ( cellArchetype ) ; PostUpdateCommands . SetComponent ( c , new LocalToWorld ( ) ) ; PostUpdateCommands . SetSharedComponent ( c , new RenderMesh ( ) { material = deadMaterial , mesh = planeMesh } ) ; PostUpdateCommands . SetComponent ( c , new Translation ( ) { Value = new float3 ( x , 0 , y ) } ) ; PostUpdateCommands . SetComponent ( c , new CellComponent ( ) { } ) ; PostUpdateCommands . SetComponent ( c , new Scale ( ) { Value = 0.1f } ) ; } } PostUpdateCommands . DestroyEntity ( entity ) ; } ) ; } }

ChangeCellStateSystem

public class ChangeCellStateSystem : JobComponentSystem { public static float TickRate = 1f ; private bool isRunning ; private float timePassed = 0f ; private struct CellStateJob : IJobForEach < CellComponent , Translation > { [ ReadOnly ] public NativeArray < CellComponent > OtherCells ; public int Width ; public int Height ; public void Execute ( ref CellComponent cell , ref Translation pos ) { int aliveNeighbours = 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x , pos . Value . z + 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x + 1 , pos . Value . z + 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x + 1 , pos . Value . z ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x + 1 , pos . Value . z - 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x , pos . Value . z - 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x - 1 , pos . Value . z - 1 ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x - 1 , pos . Value . z ) ? 1 : 0 ; aliveNeighbours + = GetStateOfCell ( pos . Value . x - 1 , pos . Value . z + 1 ) ? 1 : 0 ; if ( cell . IsAlive ) cell . ChangeTo = aliveNeighbours == 2 || aliveNeighbours == 3 ; else cell . ChangeTo = aliveNeighbours == 3 ; } private bool GetStateOfCell ( float x , float z ) { if ( x < 0 ) x = Width - 1 ; else if ( x >= Width ) x = 0 ; if ( z < 0 ) z = Height - 1 ; else if ( z >= Height ) z = 0 ; var c = OtherCells [ ( int ) ( z + x * Height ) ] ; return c . IsAlive ; } } private struct SortOtherCellsJob : IJob { public NativeArray < CellComponent > Cells ; public NativeArray < Translation > Positions ; public int N ; public int Height ; public void Execute ( ) { var cellCopy = new NativeArray < CellComponent > ( N , Allocator . Temp ) ; var posCopy = new NativeArray < Translation > ( N , Allocator . Temp ) ; for ( int size = 1 ; size < N ; size * = 2 ) { int leftLowIndex = 0 ; int k = 0 ; while ( leftLowIndex + size < N ) { int leftHighIndex = leftLowIndex + size - 1 ; int rightLowIndex = leftHighIndex + 1 ; int rightHighIndex = rightLowIndex + size - 1 ; if ( rightHighIndex >= N ) rightHighIndex = N - 1 ; int li = leftLowIndex ; int ri = rightLowIndex ; while ( li <= leftHighIndex && ri <= rightHighIndex ) { if ( CompCells ( Positions [ li ] , Positions [ ri ] , Height ) ) { cellCopy [ k ] = Cells [ li ] ; posCopy [ k ] = Positions [ li ] ; li ++ ; k ++ ; } else { cellCopy [ k ] = Cells [ ri ] ; posCopy [ k ] = Positions [ ri ] ; ri ++ ; k ++ ; } } while ( li <= leftHighIndex ) { cellCopy [ k ] = Cells [ li ] ; posCopy [ k ] = Positions [ li ] ; li ++ ; k ++ ; } while ( ri <= rightHighIndex ) { cellCopy [ k ] = Cells [ ri ] ; posCopy [ k ] = Positions [ ri ] ; ri ++ ; k ++ ; } leftLowIndex = rightHighIndex + 1 ; } for ( int i = leftLowIndex ; k < N ; i ++ ) { cellCopy [ k ] = Cells [ i ] ; posCopy [ k ] = Positions [ i ] ; k ++ ; } NativeArray < CellComponent > . Copy ( cellCopy , Cells ) ; NativeArray < Translation > . Copy ( posCopy , Positions ) ; } cellCopy . Dispose ( ) ; posCopy . Dispose ( ) ; } private bool CompCells ( Translation a , Translation b , int height ) { return a . Value . z + a . Value . x * height <= b . Value . z + b . Value . x * height ; } } protected override JobHandle OnUpdate ( JobHandle inputDeps ) { UpdateSimVariables ( ) ; if ( ShouldUpdate ( ) ) return UpdateCells ( inputDeps ) ; return inputDeps ; } private void UpdateSimVariables ( ) { if ( Input . GetKeyDown ( KeyCode . KeypadPlus ) || Input . GetKeyDown ( KeyCode . Equals ) ) { if ( TickRate > 2 ) TickRate + = 1 ; else TickRate - = 0.1f ; if ( TickRate < 0.1f ) TickRate = 0.1f ; } else if ( Input . GetKeyDown ( KeyCode . Minus ) || Input . GetKeyDown ( KeyCode . KeypadMinus ) ) { if ( TickRate >= 2 ) TickRate + = 1 ; else TickRate + = 0.1f ; } if ( Input . GetKeyDown ( KeyCode . Space ) ) isRunning = ! isRunning ; } private bool ShouldUpdate ( ) { if ( ! isRunning ) return false ; timePassed + = UnityEngine . Time . deltaTime ; if ( timePassed >= TickRate ) timePassed = 0f ; else return false ; return true ; } private JobHandle UpdateCells ( JobHandle inputDeps ) { var otherCells = GetEntityQuery ( ComponentType . ReadOnly < CellComponent > ( ) , ComponentType . ReadOnly < Translation > ( ) ) ; var cells = otherCells . ToComponentDataArray < CellComponent > ( Allocator . Persistent ) ; var pos = otherCells . ToComponentDataArray < Translation > ( Allocator . Persistent ) ; var sortOtherCellsJob = new SortOtherCellsJob ( ) { Cells = cells , Positions = pos , N = cells . Length , Height = SpawnSystem . WorldSize . y } . Schedule ( inputDeps ) ; sortOtherCellsJob . Complete ( ) ; } }

ChangeCellColorSystem

[ UpdateAfter ( typeof ( ChangeCellStateSystem ) ) ] public class ChangeCellColorSystem : JobComponentSystem { private Material deadMaterial ; private Material aliveMaterial ; private Mesh planeMesh ; protected override void OnStartRunning ( ) { base . OnStartRunning ( ) ; deadMaterial = new Material ( Shader . Find ( "Standard" ) ) { color = Color . black } ; aliveMaterial = new Material ( Shader . Find ( "Standard" ) ) { color = Color . white } ; var p = GameObject . CreatePrimitive ( PrimitiveType . Plane ) ; planeMesh = p . GetComponent < MeshFilter > ( ) . mesh ; Object . Destroy ( p ) ; } protected override JobHandle OnUpdate ( JobHandle inputDeps ) { var manager = World . DefaultGameObjectInjectionWorld . EntityManager ; Entities . WithStructuralChanges ( ) . ForEach ( ( Entity entity , ref CellComponent cell ) = > { if ( cell . IsAlive == cell . ChangeTo ) return ; cell . IsAlive = cell . ChangeTo ; manager . RemoveComponent ( entity , typeof ( RenderMesh ) ) ; manager . AddSharedComponentData ( entity , new RenderMesh ( ) { material = cell . IsAlive ? aliveMaterial : deadMaterial , mesh = planeMesh } ) ; } ) . Run ( ) ; return inputDeps ; } }

InputSystem