Dynamically generating tiles in specific shapes/regions

Godot Version

4.3

Question

Hi there!

Working on a game with 16x16 tiles. I’ve successfully created a way for the player to change 1 tile to another tile (e.g., till the land), using the set_cells_terrain_connect method. It works great, changing 1 tileset to another tileset, and connecting the tiles nicely.

However, I am wanting now to, from the player’s position, create a shape (3x3 tiles) in the direction the player is facing. I can’t seem to figure it out. I did pull surrounding tiles, but then the player is caught underneath, which I don’t want.

Attached is an image showing what I mean. I’m looking to do the left part of the image. Right now I can only do the right (green is player, yellow is changed terrain).

godot-help

Additionally, if creating this tile square of 3x3, I’d also like to prevent the tile from being placed if a cliff (or collision) will interact with any part of the 3x3 square. Hope that makes sense.

Can anyone point me in the right direction? Many thanks.

If I understand correctly, you can just loop through all the tiles that should be changed…

for x in range(3): for y in range(3):
	pass

…and check if that tile can be changed (for example is not on a cliff). You’d just have to find the “origin” of the square you’re looping through:


(The red pixels are the origins, grey is the squares, black the player)
The position of the origins relative to the player is 2 * dir + Vector2i(-1, -1) where dir is the direction the square should be placed in.

So putting that all together it should be more or less

var tile_positions: Array[Vector2i] = []
var square_orgin: Vector2i = player_position + 2 * dir + Vector2i(-1, -1)
for x in range(3): for y in range(3):
	# check if is valid tile
	tile_positions.push_back(square_orgin + Vector2i(x, y)

Now tile_positions has all the tiles that need to be changed

1 Like

First of all, thank you so much for taking the time to respond – and for such a helpful diagram. I understand generally the code you posted, and what it should do. My issue is knowing where to parse it within my code now. For some reason Godot is giving me issues with Vector2i arrays, and I’m not quite sure why. I’m also drawing null values with simple things like player.global_position.

(The error I get with the arrays is “Invalid operands ‘Vector2’ and ‘Vector2i’ in operator ‘+’.”)

The below code I am posting works – for changing 1 tile. I tried adding in the new proposed variables under the other variables, tried making a function, and tried creating range for the adding cells. My player.gd script has “var player_direction: Vector2” which works in many instances, but for some reason I am having trouble calling in this code.

You can see too that I have distance within the get_cell_under_mouse method, and that is working well and I can change that up nicely. The player’s global position is called in there, and it works – for some reason I can’t get it to work in other instances.

I think I have a gap of something foundational here on my end. For me it’s always best to learn by making mistakes and trying to solve the puzzle. I tried, so now I’m back! (For context: new to Godot!)

Thanks in advance for any further tips. Btw, the origin tiles should all be adjacent to the player, directional (top, bottom, left, right – never diagonal), and then generate the 3x3 tile square which you nicely showed with the gray.

class_name ChangeTerrainComponent
extends Node

@export var topsoil_tilemap_layer: TileMapLayer
@export var new_tilemap_layer: TileMapLayer
@export var terrain_set: int = 0
@export var terrain: int = 1

var player: Player
var mouse_position: Vector2
var cell_position: Vector2i
var cell_source_id: int
var local_cell_position: Vector2
var distance: float

func _ready() -> void:
	await get_tree().process_frame
	player = get_tree().get_first_node_in_group("player")

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("remove_water"):
			get_cell_under_mouse()
			remove_cells()
	elif event.is_action_pressed("use"):
			get_cell_under_mouse()
			add_cells()
			
func get_cell_under_mouse() -> void:
	mouse_position = topsoil_tilemap_layer.get_local_mouse_position()
	cell_position = topsoil_tilemap_layer.local_to_map(mouse_position)
	cell_source_id = topsoil_tilemap_layer.get_cell_source_id(cell_position)
	local_cell_position = topsoil_tilemap_layer.map_to_local(cell_position)
	distance = player.global_position.distance_to(local_cell_position)
	
	print("mouse_position: ", mouse_position, " cell position: ", cell_position, " cell_source_id: ", cell_source_id)
	print("distance: ", distance)
	
func add_cells() -> void:
	if distance < 50.0 && cell_source_id != -1:
		new_tilemap_layer.set_cells_terrain_connect([cell_position], terrain_set, terrain, true)

func remove_cells() -> void:
	if distance < 50.0:
		new_tilemap_layer.set_cells_terrain_connect([cell_position], 0, -1, true)
1 Like

As a general tip, I would recommend mostly using local variables and function parameters to pass around information because that makes everything more modular (other than things like permanent references to other objects or variables that are updated regularly, e.g. in _process).

“Invalid operands ‘Vector2’ and ‘Vector2i’ in operator ‘+’.” means that you are trying to add a Vector2 and Vector2i which isn’t possible in Godot. The way to fix this is to convert one of the two vectors to the other format, i.e Vector2(ivec) + vec or ivec + Vector2i(vec). (Generally I would prefer converting the Vector2i to Vector2 because that way you don’t lose the fractional numbers)

About where to place the code into the existing script, I might have misunderstood how the tiles are supposed to be placed. Do you want to place the big squares just adjacent to the player or, as your current script appears to be doing, wherever the mouse points?
If it’s under the cursor, there would be no need to calculate the origin of the square and you could just put it at mouse_position + Vector2(-1, -1) or something.
Otherwise, there would need to be some separate function that gets the direction to place the square, creates the point array (as I showed in the post before) and then calls new_tilemap_layer.set_cells_terrain_connect with that array.

1 Like

Thanks for this. Adding the Vector2 to the mouse_position was indeed the simplest way forward – I drastically overcomplicated matters.

The tile being created still has issues with originating under the player, but by increasing distance, I was able to solve that in addition to creating the 3x3 tile. I also edited the bitmask of my tileset, which seemed to help things.

Thanks again for your time! I’ll mark this particular question as solved.

Now I have a separate, but related question. I know how to instantiate scenes for, say, placing structures. And I know how to use shaders and other means to preview these buildings before the player presses “build” (or what have you).

But how might I allow the player to, in a similar way, preview the adding of tiles from a tileset onto a TileMapLayer, before pressing the button to do it?

And even more complicated: how would this preview adjust dynamically (at runtime) to the autotiling / tileset?

Open to other ideas, as well, of course! I thought as well that I could create a scene for every single tile in the tilemap, and run a check for adjacent tiletypes, to instantiate the appropriate tile. This seems extremely complicated, though.

Godot has been quite fun. I am enjoying the flexibility of scenes / nodes / signals, but finding that sometimes the simplest answer is right under my nose. A lot to get used to!

Thanks again.

1 Like

You’re welcome!
To preview the adding of tiles, I would personally instantiate an actual tileset with all the original tiles plus the ones that would be added, and put a shader on the tilemap to make it obvious somehow that it’s just a preview. If this doesn’t work for you, it’s best to create a separate topic for the issue to keep the forum organised.

1 Like