Tilemap unique per tile "visual" offset/displace

Godot Version

Godot version 4.2.1

Question

Greetings I’m prototyping a 2d isometric rpg and am trying to solve a “visual only” effect and make use of the TileMap node. In sort I wish to add a visual flare by randomly adding a slight 0-4 pixel height offset per tile to break the uniform look rather than making extra artwork.

Any guidance would be appreciated, I’m unsure if this is something achievable within the TileMap node or if it would make more sense to make my own implementation. I’d just would like to avoid making my own collision system and data structures etc…

Showing is better than telling at 4:50 of this youtube video is in essence the effect I wish to control.

https://youtu.be/04oQ2jOUjkU?t=293

I do currently have a working version of the effect in which I’ve manually created an isometric map without the use of a TileMap node but instead placing a bunch of sprites.

I’d like to use Godot’s pre existing node/system however within the TileMap I cannot see a way to recreate the effect, the closest I’ve found is making use of TileData.texture_origin however this is not unique/instanced for the tile position and applies this change to all matching tiles. (hopefully the wording made sense there)

Here’s some example code of the manual sprite veresion.

const BLOCK_SCENE   = preload("res://blocks/block.tscn")
const SPRITE_WIDTH  = 64
const SPRITE_HEIGHT = 64
var BLOCK_Y_OFFSET_RAND_RANGE = 3
var BLOCK_Y_OFFSET_RAND_SIZE = 256
var BLOCK_Y_OFFSET_RAND = []

func process_block_y_rand():
	var random = RandomNumberGenerator.new()
	random.randomize()
	for cell_x in range(BLOCK_Y_OFFSET_RAND_SIZE):
		BLOCK_Y_OFFSET_RAND.append([])
		for cell_y in range(BLOCK_Y_OFFSET_RAND_SIZE):
			var _i = random.randi_range(-BLOCK_Y_OFFSET_RAND_RANGE, BLOCK_Y_OFFSET_RAND_RANGE)
			BLOCK_Y_OFFSET_RAND[cell_x].append(Vector2i(0, _i))

func coords_to_rand(v : Vector2i):
	return BLOCK_Y_OFFSET_RAND[v.x % BLOCK_Y_OFFSET_RAND_SIZE][v.y % BLOCK_Y_OFFSET_RAND_SIZE]

func coords_to_map(v : Vector2i) -> Vector2i: 
	return  Vector2i(
		v.x*(0.5*SPRITE_WIDTH) + v.y * (-0.5*SPRITE_WIDTH), 
		v.x * (0.25*SPRITE_HEIGHT) + v.y * (0.25*SPRITE_HEIGHT)
		)

func coords_to_fullmap(v : Vector2i) -> Vector2i:
	return coords_to_map(v) + coords_to_rand(v)

func process_map_test_gen():
	var block_instance
	var coord_z = 0
	var instances = -1
	var mapped_coords : Vector2i
	for coord_x in range(20):
		for coord_y in range(20):
			instances += 1
			block_instance = BLOCK_SCENE.instantiate()
			block_instance.map_coords = Vector3i(coord_x, coord_y, coord_z)
			
			var coords = Vector2i(coord_x, coord_y)
			mapped_coords = coords_to_fullmap(coords)
			block_instance.position.x = mapped_coords.x
			block_instance.position.y = mapped_coords.y
			add_child(block_instance)

func _ready():
	process_block_y_rand()
	process_map_test_gen()

I plan to have this a “visual only” effect and have no baring on collision etc. as mentioned before any hints or guidance would be appreciated.

Unfortunately, I believe that there’s no way to change the texture offset per-cell.

You could create “alternative tiles” in the TileSet, which can each have their own
texture origin while inheriting all other properties from the source tile.
But this would be extremely tedious to set up manually, so you’ll definitely want to script it.
You could either do this with a tool script and save the alternative tiles to the resource, or you could generate the TileSet at runtime.

Edit: :sweat:

You can change individual TileData information by implementing both TileMap._use_tile_data_runtime_update() and TileMap._tile_data_runtime_update()

For example:

extends TileMap


func _use_tile_data_runtime_update(layer: int, coords: Vector2i) -> bool:
	return true


func _tile_data_runtime_update(layer: int, coords: Vector2i, tile_data: TileData) -> void:
	tile_data.texture_origin += Vector2i(0, randi_range(-10, 10))


func _process(delta: float) -> void:
	notify_runtime_tile_data_update(0)

I didn’t take any measures to avoid updating tiles that don’t need to be updated, you should do that if possible.

Result:

1 Like

Thank you @mrcdk, I was preparing myself to do what @apples suggested and create offset versions of tiles programmatically at runtime. But this is something I was hopping for and opens the door too other possibilities I was hopping to explore. :grin:

I was expecting to reply sooner but I was having login issues on mobile.
Also thanks @apples for the fast work around if it wasn’t possible. :grinning:

For sake of history @mrcdk’s example should be all you need, but here is my test using the code they provided.

extends TileMap

var BLOCK_RAND_OFFSET_RANGE = 6
var BLOCK_RAND_OFFSET_SIZE = 256
var BLOCK_RAND_OFFSET_DATA = {}

func _process_block_rand_offset():
	var random = RandomNumberGenerator.new()
	var pos : Vector2i
	var rand : Vector2i
	random.randomize()
	for cell_x in range(BLOCK_RAND_OFFSET_SIZE):
		for cell_y in range(BLOCK_RAND_OFFSET_SIZE):
			pos = Vector2i(cell_x,cell_y)
			rand = Vector2i(0, random.randi_range(0, BLOCK_RAND_OFFSET_RANGE))
			BLOCK_RAND_OFFSET_DATA[pos] = rand
			
func get_block_rand_offset_at_coords(coords : Vector2i) -> Vector2i:
	return BLOCK_RAND_OFFSET_DATA.get(Vector2i(coords.x % BLOCK_RAND_OFFSET_SIZE, coords.y % BLOCK_RAND_OFFSET_SIZE), Vector2i.ZERO)

func _use_tile_data_runtime_update(layer: int, coords: Vector2i) -> bool:
	return true

func _tile_data_runtime_update(layer: int, coords: Vector2i, tile_data: TileData) -> void:
	tile_data.texture_origin +=  get_block_rand_offset_at_coords(coords)

func _ready():
	_process_block_rand_offset()
	notify_runtime_tile_data_update(0)

2 Likes

If the tiles will remain static you don’t need to call TileMap.notify_runtime_tile_data_update() in the _process() function. Just call it when you want to update them like after _process_block_rand_offset() in _ready().

1 Like

Ah yes thanks for pointing that out, I’ve updated the code in the example.

1 Like