0 votes

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?

in Engine by (172 points)

2 Answers

+2 votes
Best answer

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.

by (172 points)

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.

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.

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.

0 votes

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:
scene setup

Sprite setup:
sprite setup

Final scene:
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.

by (19 points)
edited by
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.