INNOGAMES STORIES

DungeonBurst (Building DungeonKeeper with Unity ECS and Burst)

In this blog post I will demonstrate how to use Unity’s EntityComponentSystem as well as Burst compiled Jobs to build a dungeon builder game like the good old DungeonKeeper.
This awesome old game turned out to be the perfect example to show of some techniques that will hopefully help you to get into the mindset of DOTS and EntityComponentSystems.

I will split this into two parts, where today i will cover the following topics 
– How to define and load a GameMap
– Using BlobAssetReferences to access map information
– Using the Hybrid Renderer to build the first visualization of the gamemap
– Using CommandBuffers
– How to use Unity Physics to make objects clickable
– Changing MapTiles per mouse click
– Creating your own CommandBufferSystem

In another upcoming blog post I will cover the topics of:

– Adding worker units that will use ai to execute assigned jobs, as well as pathfinding to move around in your dungeon.
– Building an ingame dungeon job detection and distribution system for imp jobs like digging terrain and building walls and floor tiles.
– Client Input Handling
– Managing the order of execution of your EntityComponentSystems

Disclaimer:

This project is only for demonstration of usage of the Unity Data Oriented Tech Stack. Neither InnoGames nor I do own any rights on DungeonKeeper. This project is not developed by InnoGames.

This is not an absolute beginners Tutorial, you should already have familiarized yourself with the basic concepts behind Unitys new Data oriented Tech Stack, and know what the difference between Entities, Components and CommandBuffers, as well as what ComponentSystems are used for.

Project setup

Using: Unity 2019.3.15f1

Packages:
– Entities 0.11.1 preview.4
– Hybrid Renderer 0.5.2 preview.4
– Unity Physics 0.3.2 preview

How to define and load a GameMap:

The GameMap consists of a grid of MapTiles. There are multiple different materials a tile can have, also your imps can fortify territory by building walls, or floor tiles.

public enum MapTileType
{
   Empty, Water, Earth, Stone, Gold, Gems, Tile, Wall
}

There are many ways you can store and load your map data. For this project I will parse the initial map data from textures. I will use two textures:


Terrain

Territory

They are created using good old MSPaint. The dimenions of the image will define the map size, every pixel encodes the information about one tile.

The Terrain texture stores information about the tile type (f.e. Light brown = Empty, Dark brown = Earth, Yellow = Gold, Blue = Water). The Territory texture defines what Tiles are owned by which player (Player colors are red blue, yellow and green). If an empty floor tile is owned by a player, it will become a tiled floor. If an earth tile is owned by a player it will become a wall.

To make it easy to configure multiple maps, i will create a ScriptableObject GameMapConfiguration that holds the textures we are going to use, as well as a ScriptableObject MapLoaderPalette which will contain the information about what colors corresponds to what MapFieldType or Player.

Now that we have our map configuration ready, we can work on using it within the EntityComponentSystem.

Every tile of our map will hold various different data, therefore we will create one Entity for every map tile. We will store our most crucial information in the MapTile ComponentData

public struct MapTile : IComponentData
{
   public MapTileType Type;
   public int2 Position;
   public int Owner;
}

Authoring Components are a good way to convert your Unity GameObject into an Entity with ComponentData. We are not restricted to handling one single Entity, so we can use it to create all of the MapTile Entities.

public class MapLoaderAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
   public GameMapConfiguration GameMap;

   public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
   {
      //define Archetype for every MapTile
      var mapTileArchetype = dstManager.CreateArchetype(
         typeof(MapTile),
         typeof(Translation),
         typeof(Rotation),
         typeof(LocalToWorld)
      );
      int mapWidth = GameMap.Terrain.width;
      int mapHeight = GameMap.Terrain.height;
      //create entities for every tile
      var tileEntities = new NativeArray<Entity>(mapWidth * mapHeight, Allocator.Temp);
      dstManager.CreateEntity(mapTileArchetype, tileEntities);
      //get pixels from the terrain and territory textures
      var terrainPixels = GameMap.Terrain.GetPixels32();
      var territoryPixels = GameMap.Territory.GetPixels32();
      for (int y = 0; y < mapHeight; y++)
      {
         for (int x = 0; x < mapWidth; x++)
         {
            int tileIndex = x + y * mapWidth;
            var tileEntity = tileEntities[tileIndex];
            dstManager.SetName(tileEntity, $"Tile {x} {y}");
            //define owner and MapTile type by pixel color
            int owner = GameMap.Palette.GetPlayer(territoryPixels[tileIndex]);
            MapTileType terrainType = GameMap.Palette.GetTerrain(terrainPixels[tileIndex]);
            var type = terrainType;
            //owned empty tiles will become MapTileType.Tile, owned earth tiles will become MapTileType.Wall
            if (type == MapTileType.Empty && owner > 0) type = MapTileType.Tile;
            else if (type == MapTileType.Earth && owner > 0) type = MapTileType.Wall;
            //assign information to entity
            dstManager.SetComponentData(tileEntity, new MapTile
            { 
               Type = type, 
               Owner = owner, 
               Position = new int2(x, y) 
            });
            dstManager.SetComponentData(tileEntity, new Translation { Value = new float3(x + 0.5f, 0, y + 0.5f) });
            dstManager.SetComponentData(tileEntity, new Rotation { Value = quaternion.identity });
         }
      }
      tileEntities.Dispose();
   }
}

Then we create a new GameObject in our Scene and attach the ConvertToEntity script, as well as our MapLoaderAuthoring script.

When we run the game all created entities wll show up in the Entity Debugger. Of course we cannot see them in the scene yet. We will come to the visualization part soon.

Using BlobAssetReferences to access map information:

Many of the game systems will need to have some access to our map information, for example to check specific tiles of our gamemap or find neighbor tiles.

Since querying Entity information from the EntityComponentSystem does not ensure the order of Entities, we need to store them in a custom Array to make it accessible for all our ComponentSystems.

BlobAssetReferences are immutable structs that allow us to store references like Arrays and Strings, while still allowing for multithreaded processing with the Burst compiled Jobs. They are used for example by Unity’s new Physics System to store collider information, and will store animation data in the upcoming Jobs Animation System.

First we will create the struct of our BlobAsset and our GameMap ComponentData holding that reference.

public struct TileMapBlobAsset
{
   public int Width;
   public int Height;
   public BlobArray<Entity> Map;
}

public struct GameMap : IComponentData
{
   public BlobAssetReference<TileMapBlobAsset> TileMap;
}

In our MapLoaderAuthoring conversion we can now create our BlobAsset and assign it to our conversion Entity

// construct BlobAsset containing the Entity id for every cell of the map
using (BlobBuilder blobBuilder = new BlobBuilder(Allocator.Temp))
{
   ref TileMapBlobAsset tileMapAsset = ref blobBuilder.ConstructRoot<TileMapBlobAsset>();
   tileMapAsset.Width = mapWidth;
   tileMapAsset.Height = mapHeight;
   BlobBuilderArray<Entity> tileArray = blobBuilder.Allocate(ref tileMapAsset.Map, mapWidth * mapHeight);
   //copy MapTile entities to blob array
   for (int t = 0; t < mapWidth * mapHeight; t++)
   {
      tileArray[t] = tileEntities[t];
   }
   // create immutable BlobAssetReference
   var assetReference = blobBuilder.CreateBlobAssetReference<TileMapBlobAsset>(Allocator.Persistent);
   // assign BlobAssetReference to GameMap
   dstManager.AddComponentData(entity, new GameMap { TileMap = assetReference });
}

Using the Hybrid Renderer to build the first visualization of the gamemap:

To keep it simple for this example, every map tile will either be solid or not solid.
Solid tiles will consist of one Top mesh (shown in green) and up to 4 wall meshes north, east, south and west. Non solid tiles will only have a Top mesh.
When the game starts, or whenever a tile changes, we will find all required ViewParts and attach them to our MapTile Entities.
I quickly created the mesh of a small plane containing 4 small quads. I wont go into detail how i created the mesh, but I used ProBuilder which is a free package that can be found in the package manager.

Next step is to create Prefabs for all base elements of our MapTile views. They are simple GameObjects containing only the usual MeshRenderer and MeshFilter.

Out of those Prefabs I created Prefab Variants for all MapTileTypes we want to have in the game. Those variants will have the correct material with the correct texture assigned, as well as a position and rotation.

Make sure the materials have GPU Instancing enabled. This will improve the rendering speed significantly by allowing the Hybrid renderer to draw all objects with the same material at once.

Finally I create a new ScripableObject MapTileConfiguration, which simply holds references to all prefabs used for every MapTileType. Non solid tiles like Empty, Tile or Water will only have their Top Prefab assigned.

Creating the TileViewSystem

We will now start creating the first JobComponentSystem. The TileViewSystem will take our MapTileViewConfigurations, convert all our GameObject Prefabs into Entity Prefabs and store those, so we can use them to decorate our MapTiles.

public class TileViewSystem : JobComponentSystem
{
   private EndSimulationEntityCommandBufferSystem _commandBufferSystem;
   private EntityQuery _updateQuery;
   // map the int value of every MapTileType to a configuration of view prefabs
   private NativeHashMap<int, MapTileViewParts> _viewPartMap;
   // will hold all entity prefabs for a MapTileType
   private struct MapTileViewParts : IComponentData
   {
      public Entity Top;
      public Entity North;
      public Entity East;
      public Entity South;
      public Entity West;
   }

   protected override void OnCreate()
   {
      _commandBufferSystem = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
      _updateQuery = EntityManager.CreateEntityQuery(typeof(UpdateTileView));
      // only update this system when there are entities that require update
      RequireForUpdate(_updateQuery);
   }

   protected override void OnDestroy()
   {
      // make sure the map is disposed when it was initialized
      if (_viewPartMap.IsCreated)
      {
         _viewPartMap.Dispose();
      }
   }

   public void ConvertMapTileViewConfigurations(List<MapTileViewConfiguration> configurations)
   {
      _viewPartMap = new NativeHashMap<int, MapTileViewParts>(configurations.Count, Allocator.Persistent);
      // we can use using here to make sure the BlobAssetStore is disposed when we are finished
      using (var blobAssetStore = new BlobAssetStore())
      {
         var conversionSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
         foreach (var config in configurations)
         {
            var viewParts = new MapTileViewParts();
            // convert all GameObject prefabs into Entity prefabs
            viewParts.Top = GameObjectConversionUtility.ConvertGameObjectHierarchy(config.View.Top.Prefab, conversionSettings);
            viewParts.North = GameObjectConversionUtility.ConvertGameObjectHierarchy(config.View.North.Prefab, conversionSettings);
            viewParts.East = GameObjectConversionUtility.ConvertGameObjectHierarchy(config.View.East.Prefab, conversionSettings);
            viewParts.South = GameObjectConversionUtility.ConvertGameObjectHierarchy(config.View.South.Prefab, conversionSettings);
            viewParts.West = GameObjectConversionUtility.ConvertGameObjectHierarchy(config.View.West.Prefab, conversionSettings);
            // since enums do boxing we will use the int value of the MapTileType as key
            _viewPartMap.Add((int)config.TileType, viewParts);
         }
      }
   }

   protected override JobHandle OnUpdate(JobHandle inputDeps){ ... }
}

To get our MapTileViewConfigurations into the system, we will first introduce a new field …

public List<MapTileViewConfiguration> TileViewConfigurations;

… to our MapLoaderAuthoring script. 
At the end of the Convert(..) method we will find our TileViewSystem and call our ConvertMapTileViewConfigurations(..) method.

// initialize systems
var world = World.DefaultGameObjectInjectionWorld;
world.GetOrCreateSystem<TileViewSystem>().ConvertMapTileViewConfigurations(TileViewConfigurations);

Then we simply drag and drop our MapTileConfigurations to the MapLoader GameObject in our scene.

We will create two new empty ComponentData types called ViewPart and UpdateTileView which we will use to tag our Entities. ViewPart will be attached to all instantiated entities that are building the visible part of our MapTile. UpdateTileView will be attached to a MapTile entity when it requires an update. For example when the MapTileType changed and we need to update its ViewParts.

When we update the TileViewSystem we want to schedule two jobs.

The first job will find all existing ViewPart Entities in our scene and destroy them, when they are attached to a MapTile we are about to update.
The second job will update all MapTile Entities that have the UpdateTileView component attached. It will instantiate all ViewParts accociated with the MapTileType and attach them to our MapTile Entity. It is not allowed to destroy or instantiate entities while we are inside a mulithreaded Burst job, so we will make use of the CommandBuffer. This CommandBuffer will be provided by the EndSimulationEntityCommandBufferSystem. This system will playback the  CommandBuffer at the end of the frame.

protected override JobHandle OnUpdate(JobHandle inputDeps)
{
   // create concurrent commandbuffer that can be used in mulithreaded burst jobs
   var commandBuffer = _commandBufferSystem.CommandBuffer.ToConcurrent();
   // get all entities that require an update
   var updateEntities = _updateQuery.ToEntityArray(Allocator.TempJob);
   // first we wanna clean up all ViewPart Entities currently attached to the MapTiles that require an update
   // iterate over all ViewParts
   var cleanUpViewPartsHandle = Entities.WithAll<ViewPart>().ForEach((int entityInQueryIndex, Entity entity, in Parent parent) =>
   {
      // if parent requires update
      if (updateEntities.Contains(parent.Value))
      {
         // destroy ViewPart 
         EntitycommandBuffer.DestroyEntity(entityInQueryIndex, entity);
      }
      // we need to tell the compiler that we will only read the updateEntities array. when the job is complete dispose it
   }).WithReadOnly(updateEntities).WithDeallocateOnJobCompletion(updateEntities).Schedule(inputDeps);
   // second we want to instantiate all ViewParts for all MapTiles that require an update
   // we only have one Entity with GameMap componendata, so we can get it by singleton
   var mapData = GetSingleton<GameMap>().TileMap;
   // we need to read LocalToWorld and MapTile data from Entities
   var localToWorldData = GetComponentDataFromEntity<LocalToWorld>(true);
   var mapTileData = GetComponentDataFromEntity<MapTile>(true);
   var viewPartMap = _viewPartMap;
   // iterate over all tiles that need an update
   var createViewPartsHandle = Entities.WithAll<UpdateTileView>().ForEach((int entityInQueryIndex, Entity entity, in MapTile mapTile) =>
   {
      // if we have a configuration for this type of MapTile
      if (viewPartMap.ContainsKey((int)mapTile.Type))
      {
         var viewConfig = viewPartMap[(int)mapTile.Type];
         int x = mapTile.Position.x;
         int y = mapTile.Position.y;
         if (viewConfig.Top != Entity.Null)
         {
            // instantiate top ViewPart prefab
            var viewPart = commandBuffer.Instantiate(entityInQueryIndex, viewConfig.Top);
            // get position from the viewpart prefab, this will be our parent offset
            var localToParent = localToWorldData[viewConfig.Top].Value;
            // assign viewpart data
            SetupViewPart(entityInQueryIndex, viewPart, entity, localToParent, commandBuffer);
         }
         //we only want to spawn walls towards sides that are not solid and in map bounds
         if (viewConfig.North != Entity.Null && !IsSolidTile(x, y + 1, ref mapData.Value, mapTileData))
         {
            var viewPart = commandBuffer.Instantiate(entityInQueryIndex, viewConfig.North);
            var localToWorld = localToWorldData[viewConfig.North].Value;
            SetupViewPart(entityInQueryIndex, viewPart, entity, localToWorld, commandBuffer);
         }
         if (viewConfig.East != Entity.Null && !IsSolidTile(x + 1, y, ref mapData.Value, mapTileData))
         {
            var viewPart = commandBuffer.Instantiate(entityInQueryIndex, viewConfig.East);
            var localToWorld = localToWorldData[viewConfig.East].Value;
            SetupViewPart(entityInQueryIndex, viewPart, entity, localToWorld, commandBuffer);
         }
         if (viewConfig.South != Entity.Null && !IsSolidTile(x, y - 1, ref mapData.Value, mapTileData))
         {
            var viewPart = commandBuffer.Instantiate(entityInQueryIndex, viewConfig.South);
            var localToWorld = localToWorldData[viewConfig.South].Value;
            SetupViewPart(entityInQueryIndex, viewPart, entity, localToWorld, commandBuffer);
         }
         if (viewConfig.West != Entity.Null && !IsSolidTile(x - 1, y, ref mapData.Value, mapTileData))
         {
            var viewPart = commandBuffer.Instantiate(entityInQueryIndex, viewConfig.West);
            var localToWorld = localToWorldData[viewConfig.West].Value;
            SetupViewPart(entityInQueryIndex, viewPart, entity, localToWorld, commandBuffer);
         }
      }
      // remove the update tile flag
      commandBuffer.RemoveComponent<UpdateTileView>(entityInQueryIndex, entity);
      // we need to tell the compiler that we only want to read from these data containers
   }).WithReadOnly(viewPartMap).WithReadOnly(localToWorldData).WithReadOnly(mapTileData).Schedule(cleanUpViewPartsHandle);
   // make sure our jobs are finished when the commandbuffer wants to playback
   _commandBufferSystem.AddJobHandleForProducer(createViewPartsHandle);
   return createViewPartsHandle;
}

private static void SetupViewPart(int entityInQueryIndex, Entity entity, Entity parent, float4x4 localToParent, EntityCommandBuffer.Concurrent commandBuffer)
{
   commandBuffer.AddComponent(entityInQueryIndex, entity, new Parent { Value = parent });
   commandBuffer.AddComponent(entityInQueryIndex, entity, new LocalToParent { Value = localToParent });
   commandBuffer.AddComponent(entityInQueryIndex, entity, new ViewPart());
}

private static bool IsSolidTile(int x, int y, ref TileMapBlobAsset mapData, ComponentDataFromEntity<MapTile> tileData)
{
   // if coordinate is out of bounds consider it solid
   if (x < 0 || y < 0 || x == mapData.Width || y == mapData.Height) return true;
   // find MapTile entity in our TileMapBlobAsset reference
   var tileEntity = mapData.Map[x + y * mapData.Width];
   // get MapTile data for entity
   var tile = tileData[tileEntity];
   return tile.Type.IsSolid();
}

Finally we will go back to the MapLoaderAuthoring script once more and add the UpdateTileView component to our mapTileArchetype. We can run the game and all our MapTiles will get updated and their ViewParts attached.

Look at that, a fresh dungeon waiting to be claimed…

Great! finally something to look at with our evil eyes. To verify that our tile updating is working as intended, we want to be able to click on MapTiles and change their type. Luckily Unity released their Dots Physics package, which allows us to assign colliders and do raycasts.

How to use Unity Physics to make objects clickable

The next JobComponentSystem we are about to create is the TileCollisionSystem. It will work very similar to the TileViewSystem, but much simpler. It will find Entities with the UpdateTileView tag and assign the correct colliders for those. Since manually allocating colliders seems to be a bit tricky, we again go with the GameObjectToEntityConversion.

For that we need to create two prefabs with the PhysicsShape component attached. The solid one is a Box, and the Floor one is a quad. Both with dimensions 1 * 1 units

We will convert these prefabs similar to how we converted our MapTileViewConfigurations. Then we will grab their collider BlobReferences and store them, so we can use them in our System. Once again the MapLoaderAuthoring will have public fields for the physic shape prefabs and will call our systems ConvertColliderPrefabs(..) method at the end of the Convert(..) method.

//we always want to update before physics updates
[UpdateBefore(typeof(BuildPhysicsWorld))]
public class TileCollisionSystem : JobComponentSystem
{
   private EndSimulationEntityCommandBufferSystem _commandBufferSystem;
   private BlobAssetReference<Collider> _floorColliderPrefab;
   private BlobAssetReference<Collider> _solidColliderPrefab;

   protected override void OnStartRunning()
   {
      _commandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
   }

   protected override void OnDestroy()
   {
      //clean up BlobAssetReferences when initialized
      if (_floorColliderPrefab.IsCreated)
      {
         _floorColliderPrefab.Dispose();
         _solidColliderPrefab.Dispose();
      }
   }

   public void ConvertColliderPrefabs(UnityEngine.GameObject floorCollider, UnityEngine.GameObject solidCollider)
   {
      using (var blobAssetStore = new BlobAssetStore())
      {
         var conversionSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
         var floorColliderPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(floorCollider, conversionSettings);
         var solidColliderPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(solidCollider, conversionSettings);
         _floorColliderPrefab = EntityManager.GetComponentData<PhysicsCollider>(floorColliderPrefab).Value;
         _solidColliderPrefab = EntityManager.GetComponentData<PhysicsCollider>(solidColliderPrefab).Value;
         // the BlobAssetStore will contain collider information, which it would try to dispose. This data is handled by UnitPhysics, so we dont want that. resetting the cache seems to do the trick. i hope this has no unintended consequences
         blobAssetStore.ResetCache(false);
      }
   }

   protected override JobHandle OnUpdate(JobHandle inputDeps)
   {
      var commandBuffer = _commandBufferSystem.CreateCommandBuffer().ToConcurrent();
      var floorPrefab = _floorColliderPrefab;
      var solidPrefab = _solidColliderPrefab;
      // iterate over all MapTiles that require an update and already have a collider attached
      var updateColliderHandle = Entities.WithAll<UpdateTileView>().ForEach((int entityInQueryIndex, Entity entity, in MapTile mapTile, in PhysicsCollider collider) =>
      {
         var colliderType = collider.Value.Value.Type;
         var expected = mapTile.Type.IsSolid() ? ColliderType.Box : ColliderType.Quad;
         if (colliderType != expected)
         {
            // if collider is not what we expect, change it
            var data = mapTile.Type.IsSolid() ? solidPrefab : floorPrefab;
            commandBuffer.SetComponent(entityInQueryIndex, entity, new PhysicsCollider { Value = data });
         }
      }).Schedule(inputDeps);
      // iterate over all MapTiles that require an update and do not have a collider attached yet
      var addColliderHandle = Entities.WithAll<UpdateTileView>().WithNone<PhysicsCollider>().ForEach((int entityInQueryIndex, Entity entity, in MapTile mapTile) =>
      {
         var data = mapTile.Type.IsSolid() ? solidPrefab : floorPrefab;
         commandBuffer.AddComponent(entityInQueryIndex, entity, new PhysicsCollider { Value = data });
      }).Schedule(updateColliderHandle);
      // make sure our jobs are finished when the commandbuffer wants to playback
      _commandBufferSystem.AddJobHandleForProducer(addColliderHandle);
      return addColliderHandle;
   }
}

Changing MapTiles per mouse click

To handle client input like mouse and keyboard, we create a new system called ClientInputSystem. Because we do not schedule any jobs, and the update tick is executed on the main thread, it will be of type ComponentSystem.

// we always want to update this system before the TileViewSystem
[AlwaysUpdateSystem]
[UpdateBefore(typeof(TileViewSystem))]
public class ClientInputSystem : ComponentSystem
{
   private BuildPhysicsWorld _buildPhysicsSystem;
   private UnityEngine.Camera _camera;

   protected override void OnStartRunning()
   {
      _buildPhysicsSystem = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<BuildPhysicsWorld>();
      _camera = UnityEngine.Camera.main;
   }

   protected override void OnUpdate()
   {
      // when left mouse is clicked
      if (UnityEngine.Input.GetMouseButtonDown(0))
      {
         int mapTileTypes = Enum.GetValues(typeof(MapTileType)).Length;
         // we need to read and write to MapTile data
         var mapTileData = GetComponentDataFromEntity<MapTile>();
         //get current collision world
         var collisionWorld = _buildPhysicsSystem.PhysicsWorld.CollisionWorld;
         //get ray from camera
         var ray = _camera.ScreenPointToRay(UnityEngine.Input.mousePosition);
         if (RayCast(ray.origin, ray.origin + ray.direction * 100, out RaycastHit result, collisionWorld))
         {
         // get the hit entity via rigidbodyindex
            var entity = _buildPhysicsSystem.PhysicsWorld.Bodies[result.RigidBodyIndex].Entity;
            // get MapTile data
            var tileData = mapTileData[entity];
            // change MaptileType to the next type in enum
            tileData.Type = (MapTileType)(((int)tileData.Type + 1) % mapTileTypes);
            // apply data to Entity
            mapTileData[entity] = tileData;
            // we need to update the changed MapTile and all 8 surrounding neighbours
            ref TileMapBlobAsset mapData = ref GetSingleton<GameMap>().TileMap.Value;
            var position = tileData.Position;
            for (int y = position.y - 1; y <= position.y + 1; y++)
            {
               for (int x = position.x - 1; x <= position.x + 1; x++)
               {
                  if (x < 0 || y < 0 || x == mapData.Width || y == mapData.Height) continue;
                  var tileEntity = mapData.Map[x + y * mapData.Width];
                  EntityManager.AddComponentData(tileEntity, new UpdateTileView());
               }
            }
         }
      }
   }

   public static bool RayCast(float3 start, float3 end, out RaycastHit result, CollisionWorld world, uint collidesWith = ~0u, uint belongsTo = ~0u)
   {
      var input = new RaycastInput
      {
         Start = start,
         End = end,
         Filter = new CollisionFilter
         {
            BelongsTo = belongsTo,
            CollidesWith = collidesWith,
            GroupIndex = 0
         }
      };
      return world.CastRay(input, out result);
   }
}

Creating your own CommandBufferSystem

We can now click on MapTiles and change their type, its view and the view of all its neighbors will automatically get updated. we are done right? … right?

Not yet, there is a little glitch that we still need to tackle first before. To solve it, we need to have a look at our gameloop and commandbuffers.
When you click on a MapField and its type gets changed, you will see a short flickering of its mesh. But why is that?

If we take a look at our gameloop in the Entity Debugger, we can see our Systems highlighted in green. Remember we used the EndSimulationEntityCommandBufferSystem (shown in red) to destroy and create our ViewParts? they will be destroyed and instantiated right before the PresentationSystemGroup, which is basically the renderloop. We need to have the TransformSystemGroup (shown in yellow) update our new ViewParts once, before they can be rendered. One solution would be to use the BeginSimulationEntityCommandBufferSystem (shown in blue), this can be a perfectly fine solution, this would make the ViewParts getting updated at the beginning of the next gameloop. To make the game feel responsive I want these ViewParts to get updated in the exact frame the input happens. This means we have to create our own little CommandBufferSystem, which will update at the beginning of the TransformSystemGroup.

// we want this to be the first in TransformSystemGroup
[UpdateInGroup(typeof(TransformSystemGroup))]
[UpdateBefore(typeof(EndFrameParentSystem))]
public class BeginTransformCommandBufferSystem : EntityCommandBufferSystem
{
   // nothing else to implement here
}

In the TileViewSystem we will use our BeginTransformCommandBufferSystem instead of the  EndSimulationEntityCommandBufferSystem. Now we are done! Everything works smooth and without glitches.

I included the whole project in this unitypackage