Tuesday, January 14, 2020

Unity DOTS Life Tutorial entities 0.4.0 preview.10 #5 SharedComponentData & Chunk Components - GameDev.net

This tutorial covers a SharedDataComponent & Chunk Components. This will be an attempted speed optimization by reducing the number of calls to render GameObject by have each GameObject cover a  2x2 square of cells. It will show usage of SharedComponentData and ComponentData used as Chunk Component.

Build tested on entities 0.4.0 preview.10 . Packages used are shown here

The source is at https://ift.tt/35xGoch. The Zip file of the project is at  http://ryuuguu.com/unity/ECSLifeTutorials/ECSLifeTutorial5.zip

Warning: DOTS is still bleeding edge tech. Code written now will not be compatible with first release version of DOTS. There are people using the current version DOTS for release games, but they usually freeze their code to specific package and will not be able 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 a get a feel for code this type of thing in Unity.

Each entity will still represent a single cell, but GameObjects will be SuperCells display a 2x2 square of cells. There are 16 materials one for configuration 4 cells being live or dead. The ECS simulation will calculate all cells live state as before, but it will then calculate which of the 16 states each supercell is in. if a supercell has changed it will call the Monobehaviour with the new state of the supercell. The Monobehaviour will then change material of the supercell renderer to match the state. The is no entity to represent a supercell. Instead cells will grouped in to chunks with all cells in the same supercell in one chunk. The SuperCellsLives component will be added to each chunk and hold the position of the super cell, its state, and if it has changed. the state will be used as an index in to an array materials by the Monobehaviour. The SharedComponentData SuperCellXY will be associated with the chunk for the supercell at XY. First a word about SharedComponentData. From this forum thread https://ift.tt/35PS4rb

"Shared component data is really for segmenting your entities into forced chunk grouping. The name is unfortunate I think. Because really if you use it as data sharing mechanism, you will mostly shoot yourself in the foot because often you just end up with too small chunks.

BlobData is just a reference to shared immutable data. BlobData is also easily accessible from jobs and can contain complex data." Joachim Ante

So SuperCellXY will just be used in setting up the supercells and then not used again. This post on the forums about Chunk Components is a good description Chunk ComponetData. https://ift.tt/2RhfPmB

Chunk data is attached to chunk a archetype this mean Chunk data is fragile.  The same 4 cells will always be in a single chunk, but which chunk may change. For example if a you add an InitializationTag to do initialization with a special InitializeSystem then remove the InitializationTag, all the entities will move to a new chunk and lose the chunk data. This happens because the archetype of the entity has changed since it no longer has an InitializationTag.  Similarly if in your Monobehaviour code setting things up you assign the chunk data then the archetype changes the chunk data will be lost.   AddComponetData() changes the archetype. In this tutorial I set the pos data in SuperCellLives one system and use it in the next system. This is not as efficient as setting it once in the initial setup then assuming the chunk archetype will never change, but it is more robust to future code changes. There are instructions in the code for what to comment and uncomment to make the code faster but less robust.  

Now to some code. The ECSGridSuperCell Monobehaviour first setups the SuperCell GameObjects and stores their renderers in an array.

    public void InitSuperCellDisplay() {
       _scale = ( Vector2.one / size);
       _offset = ((-1 * Vector2.one) + _scale)/2;
       _meshRenderersSC = new MeshRenderer[size.x+2,size.y+2];
       materialsStatic = materials;
       var cellLocalScale  = new Vector3(_scale.x,_scale.y,_scale.x) * superCellScale;
       for (int i = 0; i < size.x+2; i++) {
           for (int j = 0; j < size.y+2; j++) {
               var coord = Cell2Supercell(i, j);
               if (coord[0] != i || coord[1] != j) continue;
               var c = Instantiate(prefabMesh, holderSC);
               var pos = new Vector3((1f/superCellScale +i-1) * _scale.x + _offset.x,
                   (1f/superCellScale +j-1) * _scale.y + _offset.y, zLive);
               c.transform.localScale = cellLocalScale;
               c.transform.localPosition = pos;
               c.name += new Vector2Int(i, j);
               _meshRenderersSC[i,j] = c.GetComponent<MeshRenderer>();
           }
       }
   }

   public int2 Cell2Supercell(int i, int j) {
       var pos = new int2();
       pos[0] = (i  / 2) * 2; //(0,1) -> 0, (2,3) -> 2, etc.
       pos[1] = (j  / 2) * 2;
       return pos;
   }

Setting the materials on these renders can not be done directly from the ECS thread because in UpdateSuperCellChangedSystem, job.Run(m_Group) does not run on the main thread and Unity throws an error. "UnityException: SetMaterial can only be called from the main thread." I do not know if this job not being on the main thread even though Run() is used is temporary 0.4 limitation or by design.  So instead the information is buffered and then executed next Update().

 public static void ShowSuperCell(int2 pos,int val) {
       var command = new ShowSuperCellData() {
           pos = pos,
           val = val
       };
       SuperCellCommandBuffer.Add(command);
   }
   
   private static void RunSCCommandBuffer() {
       foreach (var command in SuperCellCommandBuffer) {
           //Debug.Log(" ShowSuperCell: "+ command.pos + " : "+ command.val);
           _meshRenderersSC[command.pos.x,command. pos.y].enabled = command.val != 0;
           if (command.val != 0) {
               _meshRenderersSC[command.pos.x, command.pos.y].material = materialsStatic[command.val];
           }
       }
       SuperCellCommandBuffer.Clear();
   }

Initializing the entities is almost the same as before with 4 extra actions.

First calculate the relative position of the cell in its supercell encode  and place this in  SubcellIndex. Then calculate which supercell a cell is in "var pos = Cell2Supercell(i,j);" then add a SharedComponentData<SuperCellXY> with that pos to the instance. This will move the instance to the chunk associate with that SharedComponetData. Then add a AddChunkComponentData<SuperCellLives> which changes the archetype of the instance and is added to the new archetype is it not already attached.  

   void InitECS() {
           var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
           
           _cells = new Entity[size.x+2, size.y+2];
           
           for (int i = 0; i < size.x+2; i++) {
               for (int j = 0; j < size.y+2; j++) {
                   var instance = entityManager.CreateEntity();
                   entityManager.AddComponentData(instance, new Live { value = 0});
                   entityManager.AddComponentData(instance, new PosXY { pos = new int2(i,j)});
                   _cells[i, j] = instance;
               }
           }
           
           for (int i = 1; i < size.x+1; i++) {
               for (int j = 1; j < size.y+1; j++) {
                   var instance = _cells[i, j];
                   
                   entityManager.AddComponentData(instance, new SubcellIndex() {
                       index = ((i)%2) + (((j+1)%2)*2)
                   });
                   entityManager.AddComponentData(instance, new NextState() {value = 0});
                   entityManager.AddComponentData(instance, new Neighbors() {
                       nw = _cells[i - 1, j - 1], n = _cells[i - 1, j], ne =  _cells[i - 1, j+1],
                       w = _cells[i , j-1], e = _cells[i, j + 1],
                       sw = _cells[i + 1, j - 1], s = _cells[i + 1, j], se =  _cells[i + 1, j + 1]
                   });
                   
                   // New code is below here
                   var pos = Cell2Supercell(i,j);
                   entityManager.AddSharedComponentData(instance, new SuperCellXY() {pos = pos});
                   entityManager.AddChunkComponentData<SuperCellLives>(instance);
               }
           }
           InitLive(entityManager);
       }

That is it for the new Monobehaviour code.  There are 4 new components. The SharedDataComponent SuperCellXY is only called explicitly in the Monobehaviour code above.  SuperCellLives is a Chunk ComponentData because it added with AddChunkComponentData otherwise it just another ComponentData.

 /// <summary>
 /// SharedData Component
 ///   chunks cells into correct chunk
 ///    pos is only used to decide what cell goes into which Chunk
 /// </summary>
 public struct SuperCellXY : ISharedComponentData {
     public int2 pos; // these coordinates are the xMin, yMin corner
 }

 /// <summary>
 /// SuperCellLives
 ///  Chunk Component
 ///  uses lives of cells to calculate image index
 /// </summary>
 public struct SuperCellLives : IComponentData {
     public int index; //index of image to be displayed
     public bool changed;
     public int2 pos;
 }

 /// <summary>
 /// DebugSuperCellLives
 /// used for debugging SuperCellLives since the debugger
 /// is broken for ChunkComponents
 /// </summary>
 public struct DebugSuperCellLives : IComponentData {
     public int4 livesDecoded;
     public int index;
     public bool changed;
     public int2 pos;
 }

 /// <summary>
 /// SubcellIndex
 ///   relative pos of a cell in its SuperCell
 /// </summary>
 public struct SubcellIndex : IComponentData {
     public int index;
 }

DebugSuperCellLives can be added with AddComponentData() for debugging. Unfortunately chunk ComponentData does not show properly in the debugger so DebugSuperCellLives can be used to view the data since it is a regular component. There is a system to copy the data from SuperCellLives to  DebugSuperCellLives. livesDecoded is an extra variable that shows the live value of each cell in the supper cell. [0] = top Left corder,  [1] = top right [2] = bottom Left and [3] = bottom Right cell. SubcellIndex is the index value in the livesDecoded array and is calculated once in the ECSGridSuperCell Monobehaviour. It is used to calculate the correct image to be used by the SuperCell Monobehaviour. The SubcellIndex also matches the index of livesDecoded in DebugSuperCellLives.

The chunk related code is in UpdateSuperCellIndexSystem and UpdateSuperCellChangedSystem .

/// <summary>
/// UpdateSuperCellIndexSystem
///     Calculate new image index for SuperCellLives
///     Set pos of SuperCellLives
///     set changed of SuperCellLives
/// </summary>
[AlwaysSynchronizeSystem]
[BurstCompile]
public class UpdateSuperCellIndexSystem : JobComponentSystem {
   EntityQuery m_Group;

   protected override void OnCreate() {
       // Cached access to a set of ComponentData based on a specific query
       m_Group = GetEntityQuery(
           ComponentType.ReadOnly<Live>(),
           ComponentType.ReadOnly<SubcellIndex>(),
           ComponentType.ReadOnly<PosXY>(),
           ComponentType.ChunkComponent<SuperCellLives>()
       );
   }
   
   struct SuperCellIndexJob : IJobChunk {
       [ReadOnly]public ArchetypeChunkComponentType<Live> LiveType;
       [ReadOnly]public ArchetypeChunkComponentType<SubcellIndex> SubcellIndexType;
       [ReadOnly]public ArchetypeChunkComponentType<PosXY> PosXYType;
       public ArchetypeChunkComponentType<SuperCellLives> SuperCellLivesType;

       public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
           var lives = chunk.GetNativeArray(LiveType);
           var SubcellIndices = chunk.GetNativeArray(SubcellIndexType);
           var posXYs = chunk.GetNativeArray(PosXYType);
           
           var scLives = new int4();
           for (var i = 0; i < chunk.Count; i++) {
               scLives[SubcellIndices[i].index] = lives[i].value;
           }
           int index = 0;
           for (int i = 0; i < 4; i++) {
               index +=   scLives[i]<< i;
           }
           
           var pos = new int2();
           pos[0] = (posXYs[0].pos.x / 2) * 2; //(0,1) -> 0, (2,3) -> 2, etc.
           pos[1] = (posXYs[0].pos.y  / 2) * 2;
           
           var chunkData = chunk.GetChunkComponentData(SuperCellLivesType);
           bool changed = index != chunkData.index;
           chunk.SetChunkComponentData(SuperCellLivesType,
               new SuperCellLives() {
                   index = index,
                   changed = changed,
                   // for faster less robust code uncomment the 3 lines at the end of
                   // ECSGridSuperCell.InitECS() around SetChunkComponentData<SuperCellLives>
                   // uncomment the next line and comment the one after
                   //pos = chunkdata.pos
                   pos = pos
               });
       }
   }

   protected override JobHandle OnUpdate(JobHandle inputDependencies) {
       var LiveType = GetArchetypeChunkComponentType<Live>(true);
       var SubcellIndexType = GetArchetypeChunkComponentType<SubcellIndex>(false);
       var SuperCellLivesType = GetArchetypeChunkComponentType<SuperCellLives>();
       var PosXYType = GetArchetypeChunkComponentType<PosXY>();

       var job = new SuperCellIndexJob() {
           SubcellIndexType = SubcellIndexType,
           LiveType = LiveType,
           SuperCellLivesType = SuperCellLivesType,
           PosXYType = PosXYType
       };
       return job.Schedule(m_Group, inputDependencies);
   }
}

/// <summary>
/// UpdateSuperCellChangedSystem
///   Check all SuperCells
///   Call Monobehaviour to update changed SuperCells
/// </summary>
[AlwaysSynchronizeSystem]
[UpdateAfter(typeof(UpdateSuperCellIndexSystem))]
public class UpdateSuperCellChangedSystem : JobComponentSystem {
   EntityQuery m_Group;

   protected override void OnCreate() {
       // Cached access to a set of ComponentData based on a specific query
       m_Group = GetEntityQuery(
           ComponentType.ChunkComponentReadOnly<SuperCellLives>()
       );
   }
   
   struct SuperCellDisplayJob : IJobChunk {
       
       public ArchetypeChunkComponentType<SuperCellLives> SuperCellLivesType;

       public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
           var chunkData = chunk.GetChunkComponentData(SuperCellLivesType);
           if (chunkData.changed) {
              ECSGridSuperCell.ShowSuperCell(chunkData.pos, chunkData.index);
           }
       }
   }

   protected override JobHandle OnUpdate(JobHandle inputDependencies) {
       
       var SuperCellLivesType = GetArchetypeChunkComponentType<SuperCellLives>();

       var job = new SuperCellDisplayJob() {
           SuperCellLivesType = SuperCellLivesType
       };
       job.Run(m_Group);
       return default;
   }
   
}

From top to bottom the chunk related code is

1) In the query ComponentType.ChunkComponent<SuperCellLives>() is used. there is also a ComponentType.ChunkComponentReadOnly<>() method if you are only reading the data.

2) SuperCellIndexJob is an IJobChunk which call Schedule()

3) The structs are declared to access components using the same generic, ArchetypeChunkComponentType<>, for both regular ComponentData and chunk ComponentData.

4) The execute method signature is Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex), chunk can access all ComponentData either with GetNativeArray() or GetChunkComponentData()

5) Local native arrays are declared for regular ComponetData. var lives = chunk.GetNativeArray(LiveType);

6) I know that there will be always be 4 entities per chunk because exactly 4 entities are assign the same SharedDataComponent in the setup. So it is safe for scLives to fixed length array of int4.

var scLives = new int4();
for (var i = 0; i < chunk.Count; i++) {
   scLives[SubcellIndices[i].index] = lives[i].value;
}

7) SetChunkComponentData() is used to assign new chunk ComponetData.

8) In the OnUpdate() GetArchetypeChunkComponentType<Live>() is used to assign ComponentType structs.

9) In the Schedule() the EntityQuery and dependencies are passed

10) UpdateSuperCellChangedSystem runs its job on the main thread  and passes the EntityQuery in the Run() call.

In conclusion SharedDataComponent is not for sharing data but for partitioning instance in to chunks. Chunk ComponentData contains data that refers to a single chunk but can be fragile since things that intuitively would not affect it can clear the data. Specifically changing an instances archetype by adding or removing component data such as an empty flag component will clear the data by changing the archetype and some moving the instance to a new chunk. As an optimization this failed because changing a Material is expensive, but I feel it was still worth testing.

Let's block ads! (Why?)



"tutorial" - Google News
January 11, 2020 at 03:00PM
https://ift.tt/3a8wWj6

Unity DOTS Life Tutorial entities 0.4.0 preview.10 #5 SharedComponentData & Chunk Components - GameDev.net
"tutorial" - Google News
https://ift.tt/2N1vmVJ
Shoes Man Tutorial
Pos News Update
Meme Update
Korean Entertainment News
Japan News Update

No comments:

Post a Comment