Manipulating tilemaps from outside the tilemap tools seems to be impossible

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?

Thanks!

2 Likes

The current Tilemap API is terrible IMO. I have spent about 5 hours trying to write a replace tiles script.

6 Likes

@snarlynarwhal i am a complete noob here so dont quote me be here is a script i wrote for replacing tilemap tiles

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

using UnityEngine.Tilemaps;
public class ClickableTile : MonoBehaviour
{
    public bool PaintActive;
    public Tilemap tileMap;
    public Tile redTile;
    public List<Vector3> listOfTilePositions = new List<Vector3>();
    public Vector3Int cellPosition;
    public Camera cameraP;
    public GridLayout gridLayout;
    public int leftCornerX;
    public int leftCornerY;

   void Start()
    {
        for (int x = tileMap.cellBounds.xMin; x < tileMap.cellBounds.xMax; x++)
        {
            for (int y = tileMap.cellBounds.yMin; y < tileMap.cellBounds.yMax; y++)
            {
                Vector3Int localPlace = (new Vector3Int(x, y, (int)tileMap.transform.position.y));
                Vector3 place = tileMap.CellToWorld(localPlace);
                if (tileMap.HasTile(localPlace))
                {
                  listOfTilePositions.Add(new Vector3(x - leftCornerX, y - leftCornerY, 0));
          
                }
            }
        }
    }
    void fireLaser() {
      
        Vector2 CamPosGrid = cameraP.ScreenToWorldPoint(Input.mousePosition);
        cellPosition = gridLayout.WorldToCell(CamPosGrid);
        Debug.Log(cellPosition);
        if (listOfTilePositions.Contains(cellPosition))
        {
           tileMap.SetTile((cellPosition), redTile);
        }

    }
    void Update()
    {

        if (Input.GetButtonDown("Fire1"))
        {
            PaintActive = true;
        }
        if (Input.GetButtonUp("Fire1"))
        {
            PaintActive = false;
        }
        if (PaintActive == true)
        {
            fireLaser();
        }
    }
}

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;

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

private void FindReplaceableTilesInTilemap(Tilemap tilemap) {
        foreach (var position in tilemap.cellBounds.allPositionsWithin) {
            TileBase tile = tilemap.GetTile(position);
            if (tile != null) {
                HandleReplaceTile(tilemap, tile, position);
            }
        }
    }

    private void HandleReplaceTile(Tilemap tilemap, TileBase tile, Vector3Int position) {
        for (int i = 0, len = findTiles.Count; i < len; i++) {
            if (findTiles[i] == tile) {
                tilemap.SetTile(position, replaceTiles[i]);
            }
        }
    }

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.

Man, you are a life saver, thanks!

1 Like

Wow thanks, what about placing tiles on empty grid?

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:
    4361494--394762--Capture2.JPG

You can then modify the tilemap with perfect ease using SetTile with your already instantiated Tile objects like so:
4361494--394765--Capture.JPG

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.

Or tMap.RefreshTile(TileLocation) after changing the sprite would work

The following should work in Runtime:

var tile = ScriptableObject.CreateObject<Tile>();
tile.sprite = mySprite;
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 AssetDatabase.SaveAsset .

3 Likes

I may be missing something here, but ScriptableObject doesn’t contain any definitions for “CreateObject”.
Did you mean ScriptableObject.CreateInstance(); ?

Yes, you are right! I did mean ScriptableObject.CreateInstance<Tile>()!

1 Like

I found a solution. Corrected the Rule Tile script a little. Corrected the RuleMatches functions. In these functions, the program bypassed adjacent tiles and checked for identity. I changed the check for fullness.