Como carregar um Tilemap a partir de uma imagem

Aqui irei mostrar como transformar uma imagem como esta:

Em um mapa dentro de um TileMap:


A estrutura

Ela será composta de um nó pai do tipo Node2D (aqui chamado de stage_builder_tutorial), e um filho do tipo TileMap:

O nó pai irá ter um script controlador da geração de fases, e foram criadas as seguintes propriedades:

  1. Propriedade relacionadas à imagem recebida
    • stage: Texture2D -> Vai comportar a imagem que será transformada em uma fase.
  2. Propriedades relacionadas a estrutura de cada bloco (ou tile)
    • resource_tile_colors: Array -> Vai controlar qual quais as cores que serão esperadas. É um array que comporta campos do tipo Color.
    • resource_tile_ids: Array -> Vai controlar os ids dos blocos. É um array que comporta campos do tipo Int.

A solução aqui se diz criar estes arrays que possuem o mesmo tamanho para o controle de cada bloco. Existir outras formas de se controlar estas informações, mas vamos seguir desta forma neste momento.

Agora vamos mexer com o TileMap. Primeiro temos que criar um novo TileSet para este TileMap:

Em seguida vamos definir o tamanho dos blocos. Neste exemplo, deixei com 24px (tanto em Tile Size, quanto em Cell Quadrant Size):

Enquanto selecionado o TileMap, iremos adicionar os sprites através da opção TileSet na parte mais inferior da interface. Podemos importar sprites individuais ou um grupo de sprites.

Os sprites precisam ter as mesmas dimensões definidas no TileMap. Neste exemplo, cada um deles possuem 24px de altura e de largura.

Aqui vamos fazer a importação individual, arrastando as imagens dentro da aba Tiles, uma de cada vez.

E atenção em uma informação: O Id do bloco. Estes valores numéricos serão utilizados no script gerador da fase.

A configuração básica está feita. Agora vamos para…


O código

Para os apressados, como ficou a implementação:

extends Node2D

@export var stage: Texture2D
@export var resource_tile_colors: Array
@export var resource_tile_ids: Array

@onready var tilemap: TileMap = $tilemap

func _ready():
	if (stage == null):
		print("Stage does not exist")
		return
	
	read_stage_texture()

func read_stage_texture():
	var height = stage.get_height()
	var width = stage.get_width()
	
	var x = 0
	var y = 0
	
	while x < width:
		while width < height:
			var colorFromPixel = stage.get_image().get_pixel(x, y)
			
			var loop = 0
			for color in resource_tile_colors:
				if (color.is_equal_approx(colorFromPixel)):
					tilemap.set_cell(0, Vector2i(x, y), resource_tile_ids[loop], Vector2i(0, 0))
					break
				
				loop += 1
			y += 1
		x += 1
		y = 0

Para os menos apressados, uma breve explicação de cada bloco.

  1. Definição de tipos. Temos 3 propriedades customizáveis stage, resource_tile_color e resource_tile_ids. Já a variável tilemap fica responsável por pegar o recurso TileMap presente na estrutura da cena.
@export var stage: Texture2D
@export var resource_tile_colors: Array
@export var resource_tile_ids: Array

@onready var tilemap: TileMap = $tilemap
Como tilemap é filho do nó stage_builder_tutorial que possui o script, basta usar o nome dele para o código entender quem ele é.

2. Método _ready() vai observar se existe uma imagem carregada. Se não existir, não fará nada, se não chama a função read_stage_texture().

func _ready():
	if (stage == null):
		print("Stage does not exist")
		return
	
	read_stage_texture()

3. Função read_stage_texture() ficará responsável por preencher o TileMap. Este bloco ficou ligeiramente extenso, então vamos separar a explicação

func read_stage_texture():
	var height = stage.get_height()
	var width = stage.get_width()
	
	var x = 0
	var y = 0
	
	while x < width:
		while y < height:
			var colorFromPixel = stage.get_image().get_pixel(x, y)
			
			var loop = 0
			for color in resource_tile_colors:
				if (color.is_equal_approx(colorFromPixel)):
					tilemap.set_cell(0, Vector2i(x, y), resource_tile_ids[loop], Vector2i.ZERO)
					break
				
				loop += 1
			y += 1
		x += 1
		y = 0

3.1. Definição de variáveis de controle. A propriedade stage é quem contém a imagem da fase, e por ser um Texture2D podemos pegar suas dimensões. As variáveis x e y serão usadas no controle dos próximos loops.

var height = stage.get_height()
var width = stage.get_width()
	
var x = 0
var y = 0

3.2. Vamos agora para a leitura da imagem, de coluna em coluna, de pixel em pixel. Usando a função get_pixel(), temos em mãos a informação de cor daquele pixel.

while x < width:
	while y < height:
		var colorFromPixel = stage.get_image().get_pixel(x, y)

...

	y += 1
x += 1
y = 0

3.3. Temos aqui um outro loop, desta vez iterando pela variável resource_tile_colors que foi preenchida por nós.

Cada item do array, do tipo Color, é comparado pela cor do atual pixel através do is_equal_approx(). Se as cores conferirem, vamos definir aquele pixel no TileMap.

var loop = 0
for color in resource_tile_colors:
	if (color.is_equal_approx(colorFromPixel)):
		tilemap.set_cell(0, Vector2i(x, y), resource_tile_ids[loop], Vector2i.ZERO)
		break
				
	loop += 1
y += 1

Precisamos de algumas informações para a definição do pixel pelo set_cell()

  • Qual camada aquele pixel está. Como só temos uma, definimos como 0.
  • Qual a posição no TileMap. Usamos as variáveis de controle x e y dentro do novo Vector2i.
  • Qual o source_id que será usado. Usamos a propriedade resource_tile_ids para saber automaticamente qual pixel será usado.
  • Quais as coordenadas do atlas. Um source_id pode conter vários sprites, ou apenas um. Como importamos individualmente, definimos como Vector2i.ZERO.

E aqui fecho esta postagem. Em um futuro post eu vou passar um pouco mais afundo sobre as questões de camadas e as camadas de física na TileMap.