How can I modify the sprite of a particular Scriptable Tile in a Tilemap?

I’m in the early process of creating a top down 2D dungeon crawler using arrow keys for movement/interaction, and have been trying to implement a door tile. Scriptable Tiles seem pretty useful and I have been trying to get to grips with how they work, but I can’t seem to get the desired behaviour.

I have two scriptable tiles, NavigableTile and DoorTile, with DoorTile inheriting from NavigableTile.

When my player controller recieves movement input the following code checks the type of Tile the player is trying to move to and tests if it is set as isNavigable, if so player moves, if not it then checks to see if it is a closed door. If so it sets open and isNavigable to true.

    void MoveOnAxis()
    {
        Vector2Int input = m_PlayerInput.GetMoveInput(); 
        // if pressing keys on both axis default to vertical
        // no diagonal movement
        if (input.x != 0f && input.y != 0f) input.x = 0;

        if (input != Vector2Int.zero)
        {
            Vector3Int targetGridPos = m_Grid.WorldToCell(transform.position + new Vector3(input.x, input.y, 0));
            NavigableTile navTile = m_Tilemap.GetTile<NavigableTile>(targetGridPos);

            if (navTile && navTile.isNavigable)
            {
                Debug.Log("Move into Tile : " + navTile.name);

                transform.position = targetGridPos;
                m_GameManager.EndTurn();
            }
            else if (navTile is DoorTile)
            {
                if (!((DoorTile)navTile).open)
                {
                    Debug.Log("Opening a closed door");
                    ((DoorTile)navTile).SetOpen(true);
                }

                m_Tilemap.RefreshTile(targetGridPos);
                m_GameManager.EndTurn();
            }
        }
    }

My DoorTile class:

public class DoorTile : NavigableTile
{
    public Sprite openSprite;
    public Sprite closedSprite;
    public bool open;
    
    public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
    {
        base.GetTileData(position, tilemap, ref tileData);
        
        if (open)
        {
            tileData.sprite = openSprite;
        }
        else
        {
            tileData.sprite = closedSprite;
        }
    }

    public override void RefreshTile(Vector3Int position, ITilemap tilemap)
    {
        tilemap.RefreshTile(position);
    }

    public void SetOpen(bool isOpen)
    {
        if (isOpen)
        {
            open = true;
            isNavigable = true;
        } 
        else
        {
            open = false;
            isNavigable = false;
        }
    }
}

This works in a sense in that when the player tries to move to a tile containing a closed door, the door sprite changes to openSprite and the tile can be moved through. However when I include multiple doors interacting with one door seems to modify all the DoorTiles in the scene, rather than the single Tile that I want. Is it possible to modify a single instance of a scripted tile like this?

So after much digging and experimenting I have managed to solve this problem.

First off, Tilemap.GetTilemap(Vector3Int position) simply returns the base tile class of the tile, of which there is only ever one instance. Which is why editing the Tile returned by this method always changes all instances of the tile within the Tilemap.

The way to get around this is to use the handy property Instanced Game Object of Tiles which can be found in the inspector:

174129-screenshot-2021-01-13-123440.png

Using this it is possible to create a prefab that can be used to provide a unique instance for the tile which can be stored and accessed to control the behaviour of the tile. To facilitate this I have created two new classes:

Gametiles

Singleton which creates data structure used to hold tile’s instanced objects and their locations

    public class GameTiles : MonoBehaviour
    {
        public static GameTiles instance;

        // dictionary holds individual instances of InstanceTile gameobjects referenced by their position on the tilemap
        public Dictionary<Vector3Int, GameObject> InstanceTiles = new Dictionary<Vector3Int, GameObject>();

        //Awake is called when the script instance is loaded
        private void Awake()
        {
            if (instance == null)
                instance = this;
            else if (instance != this)
                Destroy(gameObject);
        }

    }

##InstanceTile##

An abstract extension of the unity Tile class, which instances its linked GO on StartUp and adds it to the data structure created by GameTiles. Tiles which require unique behaviour then implement this class.

    public abstract class InstanceTile : Tile
    {
        /// <summary>
        /// Used to differentiate between different subclasses of InstaceTile.
        /// Add an entry for each new InstanceTile type to this.
        /// </summary>
        public enum OBJECT_ID
        {
            DOOR,
            CHEST
        }

        /// <summary>
        /// ID unique to each child class. Force implementation in child class.
        /// </summary>
        public abstract OBJECT_ID ID { get; set; }


        [Tooltip("This Sprite will be shown in Editor only. Replaced at runtime with GO's sprite")]
        public Sprite baseSprite;


        /// <param name="gridPosition"> the position of tile in the tilemap </param>
        /// <param name="tilemap"> the tilemap the tile is on </param>
        /// <param name="go"> the Instanced Game Object of this tile. Set in inspector.</param>
        public override bool StartUp(Vector3Int gridPosition, ITilemap tilemap, GameObject go)
        {
            if (go)
            {
                Debug.Log("Adding instanced " + go.name + " to GameTiles.InstancedTiles at" + gridPosition);

                if (!GameTiles.instance.InstanceTiles.ContainsKey(gridPosition))
                {
                    GameTiles.instance.InstanceTiles.Add(gridPosition, go);
                }
            }
        
            return base.StartUp(gridPosition, tilemap, go);
        }

   
        // provides render information for TilemapRenderer
        public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
        {
            // as we always want to use the sprite of the GO, hide this.sprite at runtime by setting to null
            if (!Application.isPlaying)
            {
                // allows us to use gizmo like behaviour for invisible items like traps
                // this sprite will show for all tiles of this type and only in editor
                sprite = baseSprite;
            }
            else
            {
                sprite = null;
            }
            base.GetTileData(position, tilemap, ref tileData);
        }

        // must be called to observe changes to a tiledata
        public override void RefreshTile(Vector3Int position, ITilemap tilemap)
        {

            tilemap.RefreshTile(position);
        }

    }

##Door Tile##

As an example here is how I implemented a door:

    [CreateAssetMenu]
    public class DoorTile : InstanceTile
    {
        public override InstanceTile.OBJECT_ID ID { get => OBJECT_ID.DOOR; set => this.ID = value; }


        public override bool StartUp(Vector3Int position, ITilemap tilemap, GameObject go)
        {

            return base.StartUp(position, tilemap, go);

        
        }

        public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
        {
        
            base.GetTileData(position, tilemap, ref tileData);
        }

        public override void RefreshTile(Vector3Int position, ITilemap tilemap)
        {
            tilemap.RefreshTile(position);
        }
    }

The only important things I do here are implement the ID property and call the base methods of the InstanceTile class. All of the doors behaviour is instead defined in the DoorObject prefab. This did get me thinking as to why it is even necessary to go the complexity lengths of having InstanceTile be an abstract class. Why not just have unique tiles all be InstanceTiles as the behaviour is defined in the Instaced Game Object? The answer for me is that by taking my current approach I can still modify subclasses of InstanceTile to do something to all instances of that type of tile at once if I want/need to in future.


Now to access a unique tile all that is require is to do something like the following:

Gametiles.instance.InstancedTiles.TryGetValue(gridPosition, out GameObject go);
if (go == null)  return; // this is inside a function which exits if no object found at location. Outside a function you should do just not execute switch below
// process that object
switch (go.ID)
{
    case InstanceTiles.OBJECT_ID.DOOR:
        DoorControl control = go.GetComponent<DoorControl>();
        control.SetOpen(true);
        break;

   case InstanceTIles.OBJECT_ID.CHEST:
        ChestControl control = go.GetComponent<ChestControl>();
    // and so on....
}

I’ve set up my instanced game objects for tiles to have a sprite and a controller script which controls behaviour e.g. Changes a door’s sprite when it is open closed. I wont go into any detail on that as it is beyond the scope of what I was originally asking about and this answer is already quite lengthy. But this approach should hopefully offer a lot more flexibility in what kind of tiles can be created. If anyone needs a bit more explanation and/or detail on this then don’t be afraid to ask, it took me a while to get my head around how tilemaps work in Unity!

Though I am not familiar with this, I’d be surprised if there was no way to modify instances of a tile. In my experience, something like this will happen when you grab the wrong reference, and you are actually updating the prefab, instead of the instance.

Are you able to select a door tile in the inspector during play, and manually set the tile to open to see if all door tiles reflect the change? If you can do that then there should be a way.