TileMap: Partially modulate specific cells

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Monkstar

Hey,

I work on a 2D project that includes an isometric TileMap. The tiles can, e.g., be buildings. Now what I want to do is “paint” (=colormodulate) certain parts of these buildings. I would therefore have 2 graphics for these buildings, one which will remain unmodulated and one which will change its colour. It’s basically similar to how it’s done in this game where some buildings have some of their parts in the color of the player that owns them: https://www.openttd.org/screenshots/1.4-02-opengfx-1920x1200.png

The problem is, I believe a TileMap can’t do this. I can modulate ALL tiles in a tileset (which would already be insufficient as not all tiles of the same kind will need to be modulated in the same way), but not certain parts of the tile either. So from what it looks like, my only choice here is to use sprites that are placed on top of the TileMap. Is that correct? In this case, however, it is possible that other objects (e.g., units) are behind or in front of these sprites then. For example, they should be in front of a certain sprite but behind another one. Do I have to work with large z-indexes then?

Also, is it even worth it to use a TIleMap then in the first place? I mean, it’s supposed to be more efficient but then I will have to place all sorts of non-tile overlays on top of it. I’m a bit worried performance may suffer then. Although, I have to mention I will have around 1000 tiles in total, so I suppose it’s still rather small compared to what it could be.

Is my reasoning here correct? Is there any way to use a TileMap in the way I would like it to? If not, what is the best way to proceed from there? Should I lay another TileMap on top of it for the modulated parts with duplicates of unique tiles for each player that I can modulate? But that would lead to other problems, I guess.

:bust_in_silhouette: Reply From: rakkarage

Sorry not sure about 3 but in Godot 4 you can right click on any tile in the tile set editor and select “Create Alternative Tile” then change flip and modulate etc for the alternate.

Recently I got the same problem. I have found some solution and might want to share it with the world because, Google Search for this problem becomes unfruitful.

Yes, you could change individual TileData, without it affecting all the TileMap. So you could randomly modulate color for every Tile without creating special Alternative Tile to do it.

For it to works, you need to extends TileMap Node, there’s 2 function that you need override there:

bool _use_tile_data_runtime_update ( int layer, Vector2i coords )
void _tile_data_runtime_update ( int layer, Vector2i coords, TileData tile_data )

In _use_tile_data_runtime_update, you should return true if the appearance of the tile need to be updated, for example the color will change. By default it’s returning false. You could force it to return true all the time, but I guess it will impact the performance.

The second one is the one that’s the main part. _tile_data_runtime_update will be called if _use_tile_data_runtime_update returning true. It will supply you with the layer and coordinates of the tile that is changing, and also the TileData of that tile. In here, you could change the TileData without it affecting all the other Tile. It’s only changing that specific tile in that layer and coordinates.

But sometimes you need to change the tile from the external code, and handling this in TileMap seems counterintuitive. I’ve made some helpers function to accomplish this.

extends TileMap
class_name UpdateTileMap

var _update_fn: Dictionary = {} # of Dictionary of Callable

func update_tile(layer: int, coords: Vector2i, fn: Callable):
	if not _update_fn.has(layer): _update_fn[layer] = {}
	_update_fn[layer][coords] = fn
	notify_runtime_tile_data_update(layer)

func _use_tile_data_runtime_update(layer: int, coords: Vector2i):
	if not _update_fn.has(layer): return false
	if not _update_fn[layer].has(coords): return false
	return true

func _tile_data_runtime_update(layer: int, coords: Vector2i, tile_data: TileData):
	if not _update_fn.has(layer): return false
	if not _update_fn[layer].has(coords): return false
	var fn: Callable = _update_fn[layer][coords]
	fn.call(tile_data)
	_update_fn[layer].erase(coords)

With this new extension, you could update the Tile in Tilemap with:

tilemap.update_tile(0, Vector2i(4,4), func (tile_data:TileData): tile_data.modulate = Color.RED)

Basically all the change will be noted in the Dictionary, and we will notify the tilemap that something has changed with notify_runtime_tile_data_update(layer).
Then after it updates the change, it will remove the note, so it won’t be updated again in the next frame if it doesn’t change.

Hope it helps.