Search Unity

Manipulating tilemaps from outside the tilemap tools seems to be impossible

Discussion in '2D Experimental Preview' started by nemo10, Oct 18, 2017.

  1. nemo10


    Jul 25, 2017
    I've been working on my own version of a terrain tile that uses unique images for every tile direction instead of rotating them like the one in 2D extras does. (It's for a platformer, so I want visual variety in the sides/top/bottom)

    Rather than make every possible combination of tiles with an indented corner, I want to add the corner details as overlays when they're necessary.

    Unfortunately, I haven't yet found a way to do this due to weirdness in the Tilemap API.

    I ruled out the option of instantiating a game object for every tile that needed corner details and then stacking sprite renderers based on how many corners the tile had - I assume adding additional hundreds/thousands of GO's is a bad idea, though I could be wrong.

    This is where the API design gets confusing. In order to manipulate tile data, I need an instance of ITilemap. But... ITilemap is not an interface, it's a class. And Tilemap is not a subclass of ITilemap. No, ITilemap is a completely separate class that only seems to exist/be accessible when passed by the editor itself to GetTileData while painting tiles. And it's not even ITilemap - it's UnityEditor.EditorPreviewTilemap, which is not documented anywhere. Furthermore, Tilemap's API contains everything that ITilemap has, which makes the inclusion of ITilemap even more confusing. It also has a GetEditorPreviewTile() method, which seems to obviate the need for ITilemap.

    I tried to manipulate the contents of my corner overlay layers while creating the terrain tiles, but since I only have access to the ITilemap instance currently being used for the current tilemap, I seem to be able to set all the tilemap data I need to correctly while simultaneously completely screwing up the editor preview for the current tilemap layer.

    So my next idea was to handle all of this via menu item and see if I could get the correct tile info from outside the tile base API. Turns out, that's not really possible.

    Why? TileBase has no accessor to location on its own, so if I want to iterate through all tiles and then make changes based on what I find, I cannot simply call tilemap.GetTilesBlock (tilemap.cellBounds); and then iterate through the results - I have no access to the positions of those tiles AND I don't have access to an instance of ITilemap, so I can't call tile.GetTileData().

    Am I just not expected to ever make modifications to Tilemaps outside of the tilemap tools? Or is there some fundamental piece of information about the tilemap API that I missed?

    rakkarage and Sylmerria like this.
  2. snarlynarwhal


    Jan 16, 2014
    The current Tilemap API is terrible IMO. I have spent about 5 hours trying to write a replace tiles script.
    Trollypolly, KhenaB and rakkarage like this.
  3. tabrooksy


    Jan 21, 2018
    @PJ-Legendre i am a complete noob here so dont quote me be here is a script i wrote for replacing tilemap tiles

    Code (CSharp):
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.UI;
    7. using UnityEngine.Tilemaps;
    8. public class ClickableTile : MonoBehaviour
    9. {
    10.     public bool PaintActive;
    11.     public Tilemap tileMap;
    12.     public Tile redTile;
    13.     public List<Vector3> listOfTilePositions = new List<Vector3>();
    14.     public Vector3Int cellPosition;
    15.     public Camera cameraP;
    16.     public GridLayout gridLayout;
    17.     public int leftCornerX;
    18.     public int leftCornerY;
    20.    void Start()
    21.     {
    22.         for (int x = tileMap.cellBounds.xMin; x < tileMap.cellBounds.xMax; x++)
    23.         {
    24.             for (int y = tileMap.cellBounds.yMin; y < tileMap.cellBounds.yMax; y++)
    25.             {
    26.                 Vector3Int localPlace = (new Vector3Int(x, y, (int)tileMap.transform.position.y));
    27.                 Vector3 place = tileMap.CellToWorld(localPlace);
    28.                 if (tileMap.HasTile(localPlace))
    29.                 {
    30.                   listOfTilePositions.Add(new Vector3(x - leftCornerX, y - leftCornerY, 0));
    32.                 }
    33.             }
    34.         }
    35.     }
    36.     void fireLaser() {
    38.         Vector2 CamPosGrid = cameraP.ScreenToWorldPoint(Input.mousePosition);
    39.         cellPosition = gridLayout.WorldToCell(CamPosGrid);
    40.         Debug.Log(cellPosition);
    41.         if (listOfTilePositions.Contains(cellPosition))
    42.         {
    43.            tileMap.SetTile((cellPosition), redTile);
    44.         }
    46.     }
    47.     void Update()
    48.     {
    50.         if (Input.GetButtonDown("Fire1"))
    51.         {
    52.             PaintActive = true;
    53.         }
    54.         if (Input.GetButtonUp("Fire1"))
    55.         {
    56.             PaintActive = false;
    57.         }
    58.         if (PaintActive == true)
    59.         {
    60.             fireLaser();
    61.         }
    62.     }
    63. }
    Last edited: Jan 21, 2018
  4. tabrooksy


    Jan 21, 2018
    if you test my code please be aware that you will need to adjust the leftCornerX and leftCornerY to the number displayed for the cell position ie

    i you click on the bottom left corner of the tile map in playmode and it says (-8.0,-9.0,0.0) in the debug adjust to following

    LeftcornerX = 8;
    LeftcornerY= 9;
    Last edited: Jan 21, 2018
  5. snarlynarwhal


    Jan 16, 2014
    @tabrooksy thanks for the reply - I eventually figured it out and did something similar:

    Code (CSharp):
    1. private void FindReplaceableTilesInTilemap(Tilemap tilemap) {
    2.         foreach (var position in tilemap.cellBounds.allPositionsWithin) {
    3.             TileBase tile = tilemap.GetTile(position);
    4.             if (tile != null) {
    5.                 HandleReplaceTile(tilemap, tile, position);
    6.             }
    7.         }
    8.     }
    10.     private void HandleReplaceTile(Tilemap tilemap, TileBase tile, Vector3Int position) {
    11.         for (int i = 0, len = findTiles.Count; i < len; i++) {
    12.             if (findTiles[i] == tile) {
    13.                 tilemap.SetTile(position, replaceTiles[i]);
    14.             }
    15.         }
    16.     }
  6. Connorses


    Jan 28, 2016
    May I recommend making your tiles half the size, and drawing them using a 2x2 brush? This way you could write ruletiles that make up the corners of a tile, and they might fit together better. Simply draw groups of 4 ruletiles instead of trying to work with singular tiles.
  7. Jinnk


    Sep 20, 2014
    Man, you are a life saver, thanks!
    snarlynarwhal likes this.
  8. meksikietis222


    Nov 21, 2018
    Wow thanks, what about placing tiles on empty grid?
  9. LordYabo


    Feb 21, 2013
    For those like me, who poured over this and many other threads for hours, written by people who figured it out but couldn't actually get it to work in their game, there are two critical pieces you need to know that are not clear from the answers above:

    1) You CANNOT instantiate a Tilemaps.Tile through code. Stop trying. (At least for now)
    2) You need to instantiate the tiles you want to replace through the editor. Like this:

    You can then modify the tilemap with perfect ease using SetTile with your already instantiated Tile objects like so:

    FetchReplaceableTile(string spriteName) simply translates from the sprite I want to wherever the Tile is in the list above so i don't have to futz with index numbers.

    Works perfectly.

    To answer the other question of a blank tilemap, i'm pretty sure this will still work. In my game I made a prefab of a blank tilemap and use an editor script to fill it. So I know for sure it works with a vanilla prefab tilemap.
  10. wforrest


    Oct 3, 2018
    Or tMap.RefreshTile(TileLocation) after changing the sprite would work
  11. ChuanXin


    Unity Technologies

    Apr 7, 2015
    The following should work in Runtime:

    Code (CSharp):
    2. var tile = ScriptableObject.CreateObject<Tile>();
    3. tile.sprite = mySprite;
    4. tilemap.SetTile(position, tile);
    If you want to persist the created Tile in Editor, you will need to convert it to an actual asset in the project using
    Code (CSharp):
    1. AssetDatabase.SaveAsset