Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Tile map system

Discussion in 'Entity Component System' started by MadboyJames, Oct 2, 2019.

  1. MadboyJames

    MadboyJames

    Joined:
    Oct 28, 2017
    Posts:
    262
    This is sort of a DOTS question, although its more of a "I've never done this before, how does one solve this problem" question. I have a multiHashMap filled with "TileData". I can get the data about a specific tile by clicking over the tile, and accessing the information through turning the mouseposition into a hashIndex, and looking up the tileData in the multiHashMap. My issue is I don't know how to approach being able to "paint" tiles in the editor and save the tiles as an array to be used whenever the map needs to be loaded. Part of the issue is that these tiles are strictly structs of data. They are not entities, and are not rendered on the screen. I tried making each tile an entity with a translation, rotation, localToWorld and renderer component, but I got rather disappointing performance with just 10,000 of these entities.

    So I think keeping the TileData strictly as data is probably best. My current idea (which I do not know how to do) is to somehow paint a terrain gameobject with different textures (I don't know of a better way than a terrain gameobject, but if there is a more tile-friendly collision box generating technique, please, do tell) , hit a "save" button (which will require a custom editor, I can do that much), and the custom editor will look at the terrain object and translate each tile into data (such as resource type, if walkable, if buildable, if has status (and what the status is)). I suppose that if I use this system, then anything that changes the tile data will need to reflect the change graphically on the terrain object (such as a tile filled with lava is now cooled/ drained).

    For reference this is a top down, perspective RTS like StarCraft, WarCraft, They are Billions, and Star Wars: Empire at War. I'm not using tiles in a heavy simulation fashion like in Oxygen Not Included, or Barotrauma.

    currently I have a terrain object 100 by 100 units wide, and I set in code the number of tiles in the map (10 tiles a unit, for 10,000 tiles). The type of tile I set is just a default tile as a placeholder until I get a more precise way of calculate tile locations. Here is my relevant code to the tilesystem.

    Code (CSharp):
    1. //this is in the GameSceneManager class
    2.  
    3. public MapData mapSpawnData;
    4.  
    5. private void Awake()
    6. {
    7. mapSpawnData = new MapData
    8.         {
    9.             length = 10,
    10.             width = 10,
    11.             bottomLeftCornerOrigin=new float3(0,0,0),
    12.             tiles= new TileData[10000]
    13.         };
    14.  
    15. //This is in the QuadrantSystem class. This is where all my tile
    16. //code is. It is called the quadrant system because I also have
    17. //the map broken into quadrants. The stated map size above is
    18. //in quadrants, not tiles. Quadrants help localize unit targeting.
    19. //There are 10x10 tiles in a quad.
    20.  
    21. public static NativeMultiHashMap<int, MapQuadData> quadrentMultiHashMap;
    22.     public static NativeMultiHashMap<int, TileData> tileMulitHashMap;
    23.     public const int mapQuadrentCellSize = 10;
    24.     public const int mapQuadrantZMultiplier = 1000;
    25.     public const int tileQuadrantCellSize = 1;
    26.     public const int tileQuadrantZMultiplier = 10000;
    27.  
    28. protected override void OnCreate()
    29.     {
    30.         quadrentMultiHashMap = new NativeMultiHashMap<int, MapQuadData>(0, Allocator.Persistent);
    31.         tileMulitHashMap = new NativeMultiHashMap<int, TileData>(0, Allocator.Persistent);
    32.  
    33. base.OnCreate();
    34. }
    35.  
    36. MapData map = GameSceneManager.instance.mapSpawnData;
    37.         int length = map.tiles.Length;
    38.         tileMulitHashMap.Capacity = length;
    39.  
    40.         TileData defaultTile = new TileData
    41.         {
    42.             walkable = true,
    43.             height = new float2(0, 0),
    44.             position = new float2(0, 0),
    45.             buildable = true,
    46.             structureBuiltOn = false,
    47.             resource = 0,
    48.             //status = new int[0] Since status is an int array, I
    49. //couldn't put it in a struct. I may just assing status1 2 and 3,
    50. //because I doubt that more than 3 status can be on a tile at
    51. //once. I currently have no plans to implement status, I just
    52. //want the option to be open should I decide at a later date to
    53. //encompass them.
    54.         };
    55.  
    56.         for (int i = 0; i < length; i++)
    57.         {
    58.             int hashMapKey = GetTilePositionHashMapKey(new float3(i%(map.length*10),0,math.floor(i/(map.width*10)))); //(tileQuadrantCellSize * i) + math.floor((i / (map.width * 10)))
    59.             // tileMulitHashMap.Add(hashMapKey, map.tiles[i]);
    60. //This is how I would like to assign the tileData, but since the
    61. //map is currently generated at runtime, and not from a
    62. //predefined tile array, I need to use newTile.
    63.             TileData newTile = defaultTile;
    64.             newTile.position = new float2(i % (map.length * 10), math.floor(i / (map.width * 10)));
    65.             tileMulitHashMap.Add(hashMapKey, newTile);
    66.         }
    67. }
    68.  
    69.  protected override void OnDestroy()
    70.     {
    71.         tileMulitHashMap.Dispose();
    72.         quadrentMultiHashMap.Dispose();
    73.         base.OnDestroy();
    74.     }
    75.  
    76. private static int GetTilePositionHashMapKey(float3 position)
    77.     {
    78.         return (int)(math.floor(position.x / tileQuadrantCellSize) + (tileQuadrantZMultiplier * math.floor(position.z / tileQuadrantCellSize)));
    79.     }
    Any help is appreciated. If someone thinks that this thread is too far off from DOTS and should be posted elsewhere, please say so.
     
  2. ndesy

    ndesy

    Joined:
    Jul 22, 2019
    Posts:
    20
    What
    Which part gave you disappointing performance? 10000 entities should not be an issue. Did you enable gpu instancing on your materials?
     
    MadboyJames likes this.
  3. MadboyJames

    MadboyJames

    Joined:
    Oct 28, 2017
    Posts:
    262
    It took and extra 50ms to process the extra entities. I had a quad mesh and a copy of the default material (with GPU instancing on) as my material.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    Was it the same reference or were you duplicating the material for each Entity? Otherwise I suspect the issue is your are iterating over all 10000 entities every frame on the main thread.
     
    MadboyJames likes this.
  5. MadboyJames

    MadboyJames

    Joined:
    Oct 28, 2017
    Posts:
    262
    Here is the code I used to make a "map"
    Code (CSharp):
    1.         EndInitializationEntityCommandBufferSystem buffer = World.Active.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
    2.         EntityCommandBuffer buf = buffer.CreateCommandBuffer();
    3.  
    4.         EntityArchetype tileArchetype = World.Active.EntityManager.CreateArchetype(
    5.              typeof(Translation),
    6.               typeof(Rotation),
    7.                typeof(LocalToWorld),
    8.                 typeof(RenderMesh)
    9.              );
    10.  
    11.         for (int i = 0; i < mapSpawnData.tiles.Length; i++)
    12.         {
    13.             if (i < 400) { mapSpawnData.tiles[i] = planeTileLVLOne; }
    14.             else if (i >= 400 && i < 405) { mapSpawnData.tiles[i] = planeRampLvlOneTwo; }
    15.             else { mapSpawnData.tiles[i] = planeTileLVLTwo; }
    16.  
    17.             Entity newTile = buf.CreateEntity(tileArchetype);
    18.             tileMaterial.color = mapSpawnData.tiles[i].tileColor;
    19.             buf.SetSharedComponent(newTile, new RenderMesh { mesh = tileMesh, material = tileMaterial, layer = groundLayer });
    20.             buf.SetComponent(newTile, new LocalToWorld { });
    21.             buf.SetComponent(newTile, new Translation
    22.             {
    23.                 Value = new float3(
    24.                 (i * QuadrantSystem.tileQuadrantCellSize) % (mapSpawnData.length * 10),
    25.                 ((mapSpawnData.tiles[i].height.x + mapSpawnData.tiles[i].height.y) / 2) / 1.5f,
    26.                 math.floor(i / (mapSpawnData.length * 10)))
    27.             });
    28.             buf.SetComponent(newTile, new Rotation { Value = Quaternion.Euler(mapSpawnData.tiles[i].rotation) });
    29.         }
     
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    Ok. Not the material. You can check the Entity Debugger to make sure chunks have more than one tile in them. I know people had issues in the past assigning shared components through code rather than instantiating prefabs.

    Otherwise, show code of where the 50 ms spike is happening. Are you using Entities.ForEach on your tiles?
     
    MadboyJames likes this.
  7. ndesy

    ndesy

    Joined:
    Jul 22, 2019
    Posts:
    20
    EntityCommandBuffer has really poor performance when dealing with a lot of commands on the same frame. From my experience, it's more efficient to batch these commands on the main thread when creating a lot of entities. You can create an entity prefab with the shared component and create all entities at once in a NativeArray. You can also put all on your components in NativeArray and assign them in batch with EntityQuery.CopyFromComponentDataArray(). Here is a code sample :

    Code (CSharp):
    1. EntityArchetype archetype = EntityManager.CreateArchetype(
    2.     typeof(ParentId),
    3.     typeof(RenderData),
    4.     typeof(WorldTransform),
    5.     typeof(ParentViewId),
    6.     typeof(PixelCube),
    7.     typeof(Position),
    8.     typeof(Rotation),
    9.     typeof(Scale),
    10.     typeof(RenderColor),
    11.     typeof(MoveTo),
    12.     typeof(RotateTo),
    13.     typeof(MouseInteractive),
    14.     typeof(Prefab)
    15. );
    16.  
    17.  
    18. var prefab = EntityManager.CreateEntity(archetype);
    19. EntityManager.SetSharedComponentData(prefab, renderData);
    20. EntityManager.SetSharedComponentData(prefab, parentView.ParentViewId());
    21.  
    22. var count = _width * _height;
    23. var entities = new NativeArray<Entity>(count, Allocator.Temp);
    24. EntityManager.Instantiate(prefab, entities);
    25.  
    26.  
    27.  
    28.  
    29. var colors = new NativeArray<RenderColor>(count, Allocator.TempJob);
    30. var pixelCubes = new NativeArray<PixelCube>(count, Allocator.TempJob);
    31. var positions = new NativeArray<Position>(count, Allocator.TempJob);
    32. var rotations = new NativeArray<Rotation>(count, Allocator.TempJob);
    33. var scales = new NativeArray<Scale>(count, Allocator.TempJob);
    34. var parentIds = new NativeArray<ParentId>(count, Allocator.TempJob);
    35.  
    36. for (int x = 0; x < _width; ++x)
    37. {
    38.     for (int y = 0; y < _height; ++y)
    39.     {
    40.         var index = y * _width + x;
    41.  
    42.         parentIds[index] = parentId;
    43.         colors[index] = new RenderColor {Value = _colorSource.getColor(index)};
    44.         pixelCubes[index] = new PixelCube {Position = new int2(x, y)};
    45.         positions[index] = new Position {Value = new float3(x, y, 0)};
    46.         rotations[index] = new Rotation {Value = quaternion.identity};
    47.         scales[index] = new Scale {Value = new float3(1, 1, 1)};
    48.     }
    49. }
    50.  
    51.  
    52.  
    53. var query = EntityManager.CreateEntityQuery(
    54.     typeof(ParentViewId),
    55.     typeof(PixelCube),
    56.     typeof(Position),
    57.     typeof(Rotation),
    58.     typeof(Scale),
    59.     typeof(RenderColor),
    60.     typeof(ParentId));
    61.  
    62. query.SetFilter(parentView.ParentViewId());
    63. query.CopyFromComponentDataArray(parentIds);
    64. query.CopyFromComponentDataArray(pixelCubes);
    65. query.CopyFromComponentDataArray(colors);
    66. query.CopyFromComponentDataArray(positions);
    67. query.CopyFromComponentDataArray(rotations);
    68. query.CopyFromComponentDataArray(scales);
    69.  
    70.  
    71. entities.Dispose();
    72. colors.Dispose();
    73. pixelCubes.Dispose();
    74. positions.Dispose();
    75. rotations.Dispose();
    76. scales.Dispose();
    77. parentIds.Dispose();
     
    MadboyJames and PaulUsul like this.
  8. PaulUsul

    PaulUsul

    Joined:
    Nov 20, 2012
    Posts:
    29
    That's brilliant ndesy, thanks!! I can't wait to use that, I wish I'd found that sooner it's not in the manual
     
  9. MadboyJames

    MadboyJames

    Joined:
    Oct 28, 2017
    Posts:
    262
    Okay, so that did work to improve my performance drastically! Thank you. Back to my initial question, how would I (in the editor), "paint" tiles in the world that then are turned into an array of TileData, which I can access to generate a preset map?

    I suppose it does not have to be in the editor.