- Home /
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?
Answer by BigStuuu · Jan 13, 2021 at 02:10 PM
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:
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!
Answer by Derek_Brouwer · Jan 07, 2021 at 10:48 PM
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.
As am I, which is why I am puzzled! I believe scriptable tiles are slightly different to prefabs/scriptable objects unfortunately. You can create a Tile asset that can be painted onto a tilemap but painted tile instances do not show in the hierarchy and can't be clicked on in the editor/viewed in inspector. As far as I can tell this culling of features is to make Tiles more lightweight for efficiency when using large tilemaps. Only the properties of the tile asset (prefab?) can be changed in the inspector, but as with my code this changes all instances of that tile...
I was only woeking with the tile map for a little bit and i didn;t know the next step on controlling the tiles so a 'Scriptable Tile' is what i'd need then? still unclear on that part. project is set on backburner till i learn more. Other projects on the go anyway. one thing i did notice though is that in the tile map editor you can copy a tile and it makes a new instance prefab. if you change anything on that one tile asset they all change. so when i was trying to rotate a tile they all did and it was super annoying so i looked into it and found that copying it and making another was able to rotate the new without effecting the original. (When you copy in the editor window and past it makes a new asset) i used this for corners of water and pathway tiles for instance and it helped make the scene look more seemless when i was missing the tiles in my sprite-sheet. Not sure if this (making duplicate assets) is a solution to your problem though, this way you would need a new door instance for ever door. (which might make sense if you are giving them ID's and calling them from a list to move the char to different locations on a map for example) but most likely unnecessary.
Your answer
Follow this Question
Related Questions
Need tiles to contain data or a reference 0 Answers
Some questions about the Tilemap API 1 Answer
2D Extras Error 4 Answers
Object Squeezing Between Tiles 0 Answers
How do I set up my sprites/skeletons to correctly interact with isometric sorting layers? 2 Answers