Modulate individual tilemap tiles

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

I’m trying to set up a color-coding system on a tilemap. Each tile may require a different shape to color-code, and each tile may require a different color. There are around 10 distinct shapes and 15 distinct colors. My approach was to manually create sprites for each shape, and then use the modulate property to set the color. However, it seems as if I can’t modulate an individual tile on a tilemap.

The most recent answers I could find stated this was not possible. However, the answers seemed to be for engine version 2.1. Is it possible to modulate a tile now, or do I have to use a different method? If so, what is a good alternative method?

:bust_in_silhouette: Reply From: Bush2Tree

SOLVED: I wrote an alternative tilemap

extends Node2D

export var tile_size_x = 32
export var tile_size_y = 32
export var tile_offset_x = 0
export var tile_offset_y = 0

export var paths_to_tiles = [""]
var tile_textures = []

var tiles = {}

func _ready():
    for tile in paths_to_tiles:
        tile_textures.append(load(tile))

func set_cell(x, y, index):
    if(index == -1):
        if(tiles.has(Vector2(x, y))):
            tiles[Vector2(x, y)].queue_free()
    elif(index >= 0):
        var sprite = Sprite.new()
        sprite.visible = false
        tiles[Vector2(x, y)] = sprite
        sprite.texture = tile_textures[index]
        sprite.position.x = (x * tile_size_x) + tile_offset_x
        sprite.position.y = (y * tile_size_y) + tile_offset_y
        sprite.visible = true
        add_child(sprite)
    else:
        get_tree().quit()

func set_cellm(x, y, index, col):
    if(index == -1):
        if(tiles.has(Vector2(x, y))):
            tiles[Vector2(x, y)].queue_free()
    elif(index >= 0):
        var sprite = Sprite.new()
        sprite.visible = false
        tiles[Vector2(x, y)] = sprite
        sprite.texture = tile_textures[index]
        sprite.position.x = (x * tile_size_x) + (tile_size_x / 2) + tile_offset_x
        sprite.position.y = (y * tile_size_y) + (tile_size_y / 2) + tile_offset_y
        sprite.modulate = col
        sprite.visible = true
        add_child(sprite)
    else:
        get_tree().quit()

func set_cell_modulate(x, y, col):
    if(tiles.has(Vector2(x, y))):
        tiles[Vector2(x, y)].modulate = col

Due to being based off of instanced sprites, it most likely has worse performance than the built-in tilemap. It also obviously has far less features. However, it works for what I need to do.

It exports 5 variables. tile_size_x and tile_size_y are pretty self-explanatory. tile_offset_x and tile_offset_y will move each cell a certain distance. paths_to_tiles is a list of strings, and functions as a crude TileSet. Unfortunately, the editor interface for this variable is very clunky, so users must manually select string to be the type of every single entry, and users must manually enter every path.

set_cell does the same thing as it does for the built-in TileMap. set_cellm is like set_cell, but it also sets the modulate of the cell. set_cell_modulate sets the modulate of a cell.

I really like the idea of your custom solution from a practicality standpoint. If it works for you, then more power to you.

Another approach would be to programmatically generate the tileset with the color + shape variations you’re looking for, which would save you the trouble of creating a bunch of sprite textures by hand.

normalocity | 2022-02-23 20:43

I also discovered that you can set the modulate of a tile in the tileset (rather than trying to override the modulate of a cell in the tilemap), which means you can use a small bit of code to change the tile in a cell, accomplishing your original goal while using the power of the TileMap to its fullest.

The other potential benefit to using a TileSet + TileMap is that you can do things such as change the modulate of a single tile, and that modulate will affect all cells referencing that tile.

YMMV, but I’ve been looking for a solution similar to the one you’ve outlined, and I have a feeling that programmatically constructing the TileSet to make all the combinations you want from a simple base texture might accomplish what you want.

The total number of combinations of all of these things can, of course, start to get large very quickly, but it’s something to think about.

normalocity | 2022-02-23 20:47

Another possibility is, rather than extend from Node2D, try extending TileMap or TileSet instead, adding the programmatic features you want while inheriting the other capabilities.

As you might have guessed, I came to your question while looking for a similar solution myself. I have a very simple prototype where I have 2 shapes, and 8 players (with a unique color per player). I plan to accomplish this by having ONLY TWO textures - a square and a circle. I will load those textures, then add them to a TileSet once for every player (8x), setting the modulate on each texture as I add it the TileSet. If this works, then I’ll wind up with a 16-tile TileSet, but I only have to maintain two base images and a little bit of code for all of this.

normalocity | 2022-02-23 20:52

:bust_in_silhouette: Reply From: normalocity

So the experiment that I mentioned in comments totally worked.

  1. Have your shapes be your base tile textures
  2. Programmatically create the TileSet on your TileMap
  3. As you’re creating tiles on the TileSet, also set the modulate for each Tile

In this way I was able to create a single set of shapes (just two .png files), and dynamically create colors for every player. I then can set the cells in the TileMap according to the player that controls that cell.

So, in your example (10 shapes, 15 colors) I think you’d only have to maintain:

  1. The 10 shape images
  2. The list of 15 colors

… and your code would programmatically create a TileSet with 150 tiles in it. Then you just need a bit of logic in your code to select the correct shape + color tile and you’re set.

Here’s my code, and some screenshots to show what I did:

extends Control

const MWID = 32
const MHGT = 32

const PLAYER_COLORS = [
	Color("009BB8"), # light blue
	Color("B80D00"), # very red
	Color("158C00"), # medium green
	Color("D4BC15"), # medium yellow
	Color("004BB8"), # dark blue
	Color("26E800"), # bright green
	Color("E89200"), # medium orange
	Color("C800A4")  # purple
]

const BASE_TEXTURES = {
	"tower": preload("res://sprites/tower.png"),
	"pool": preload("res://sprites/pool.png")
}

onready var map = $map

func _ready() -> void:
	var ti
	var pi
	
	for p in range(PLAYER_COLORS.size()):
		ti = p * 2
		pi = (p * 2) + 1
		
		map.tile_set.create_tile(ti)
		map.tile_set.tile_set_texture(ti, BASE_TEXTURES.tower)
		map.tile_set.tile_set_modulate(ti, PLAYER_COLORS[p])
		
		map.tile_set.create_tile(pi)
		map.tile_set.tile_set_texture(pi, BASE_TEXTURES.pool)
		map.tile_set.tile_set_modulate(pi, PLAYER_COLORS[p])
	
	set_tiles()
	
func set_tiles() -> void:
	for ti in map.tile_set.get_tiles_ids():
		map.set_cell(ti, 0, ti)

Scene setup:

Sprite setup:

Final scene:


So, now that we’ve covered the basics let’s take this a bit further. Let’s say that you were working on a game that was very TileMap-based, and let’s say that you wanted a feature such that when you hover your mouse cursor over a tile you want that tile to display at 50% opacity, or with a gray overlay or something. What you could potentially do when you’re programmatically generating your TileSet is to duplicate all your tiles - one set was full opacity, and the rest were 50% opacity, and the offset between the two could be chosen in code at runtime as your mouse moves around the screen. This would increase the memory footprint of your TileSet, but it would also:

  • Decrease the total number of images you needed to manage significantly
  • Decrease the total game binary size, because there are fewer total images in the first place

You could do some more optimizations based on your use case, but as for something quick + easy + practical, this would be relatively simple.


Alright, finally, you could also extend TileSet to make this all automatic. Meaning, I think you could attach a normal-looking TileSet to your TileMap, use all the TileSet editing stuff in Godot, but on game load you could programmatically expand + duplicate the tiles in the TileSet to get your colors and transparency, etc. This way your TileSet (in the editor) would look very simple and small, but once the game starts and your code expands that TileSet you can make an n-dimensional TileSet from your base with very little effort.

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.

2 Likes

Thanks for sharing this. Do you have a working sample of this that is open source somewhere that I can look at? I have been trying to implement something like this so I tried the code you shared here and it I am seeing updates on tiles that I am not passing to update_tile. It may be a bug in the code that I am calling update_tile after adding logs throughout I can’t find the source of the problem.

Hello, here is some minimal code to show case the code. Actually I change it a little bit, so after updating, it should not erase the dictionary, because I’ve just realized, if you erase it, it will come back to the original tile.

1 Like

I create an account just to say that work like a charm and thank you sir. For more devs like you! :smile:

1 Like

That change was all I needed. Thanks so much for this. Awesome work!

1 Like