I would have thought this would be a fairly commonly posted idea but I've seen very little on this and most videos are purely about setting up TileMaps rather than properly using them aside from MouseClick stuff and the like. I'm looking at having an area2D overlap with a specific tile ID. I've read in the documentation that this is entirely possible but there doesn't seem to be the type of examples that I'm thinking of out there or the code I find is completely unrelated.

  • Lethn
    Here's a method that returns map space rect given a global rect as an input:

    extends TileMap
    
    func global_rect_to_map(rect: Rect2) -> Rect2i:
    	var rect_map: Rect2i
    	rect_map.position = local_to_map(to_local(rect.position))
    	rect_map.size = local_to_map(to_local(rect.position + rect.size)) - rect_map.position + Vector2i(1, 1)
    	return rect_map

    And here's how you can get the global rect of a rectangle collider parented to area2d:

    var area_rect_global: Rect2
    area_rect_global.position = $area.to_global($area/shape.position - $area/shape.shape.size * .5)
    area_rect_global.size = $area.to_global($area/shape.position + $area/shape.shape.size * .5) - area_rect_global.position

    Example usage (setting all tilemap cells that touch the area rect):

    for j in area_rect_global.size.y:
    	for i in area_rect_global.size.x:
    		var v = area_rect_global.position + Vector2i(i,j)
    		$tilemap.set_cell(0, v, 0, Vector2i(0,0))	

Holy hell that was obscure LOL! Ended up finding the answer in the documentation, I think I'm getting better at coding, small example which I hope helps people, I'll need to do some major tweaking to my code though to get it working how I want exactly but I think I can manage it, the direct documentation reference will be more useful though I think for explaining things. There's actually some quite interesting stuff in here once you break it down I just hadn't worked out that the collider signal picks up the TileMap node first so you have to give the code instructions on the data it needs to grab. You don't seem to collide with the tiles directly or anything at least that's the case with the Area2D.

https://docs.godotengine.org/en/stable/classes/class_tilemap.html#class-tilemap-method-get-used-cells-by-id

Vector2i[] get_used_cells_by_id ( int layer, int source_id=-1, Vector2i atlas_coords=Vector2i(-1, -1), int alternative_tile=-1 ) const

Returns a Vector2i array with the positions of all cells containing a tile in the given layer. Tiles may be filtered according to their source (source_id), their atlas coordinates (atlas_coords) or alternative id (alternative_tile).

If a parameter has it's value set to the default one, this parameter is not used to filter a cell. Thus, if all parameters have their respective default value, this method returns the same result as get_used_cells.

A cell is considered empty if its source identifier equals -1, its atlas coordinates identifiers is Vector2(-1, -1) and its alternative identifier is -1.

func _on_body_entered(body):
	body.get_used_cells_by_id(0)
	print(body.get_used_cells_by_id(0))

My idea is making sure that only specific tile IDs can be built on and then if the area2D detect anything else prevent any placement, should be fine with some if statements.

Okay scratch this, back to the drawing board lol, I am absolutely baffled by the documentation examples, it seems there's multiple fields I need to understand to grab the cell ID and I don't know how to use them properly and the tutorials I find are just so unhelpful for this particular usage with the TileMaps. Custom Data Layers look very interesting but even the examples I've seen there seem pretty silly and overly complicated in typical programmer fashion.

What I'm looking for is.

. Check overlap for tiles with Area2D

. Grab tiles within Area at frame

. Check the tiles have the correct ID

I would have thought I'd simply be able to do a check with an if statement but I don't know how to return a tile ID through the body without knowing what it is.

If Area2D is always a rectangle then tiles that are within are implicit. Or you want to be able to use any area shape?

Just a standard rectangle is fine, I'm trying to use the Area2D overlap for building placement, which made way more sense in my head than the syntax I found lol.

OH! Hold up, yes the body prints out the amount of cells within the TileMap, what I'm struggling with is doing an if statement from that to check the ID of the returned cells. Part of the problem is that since Godot 4 the syntax has changed drastically too so any examples I find are really out of date.

I guess what I'm trying to write is I don't understand why I can't do something simple like if body.cell_id == 0

extends Area2D

@onready var barracksPlacementOutline_Green = $BarracksPlacementOutline_Green
@onready var barracksPlacementOutline_Red = $BarracksPlacementOutline_Red

func _on_body_shape_entered(body_rid, body, body_shape_index, local_shape_index):
	var collidedTiles = []
	collidedTiles = body.get_used_cells(0)
	print(collidedTiles.size())

Get used cells is the more right idea, the overlap is happening correctly, the cells decrease in amount as I move the area2D off the tiles, however what I want to be able to do is to distinguish per tile through a for loop whether they're either the correct ID or maybe use custom data layers to distinguish whether I can build or not on them, which you think wouldn't be difficult but I've ranted about this before already.

extends Area2D

@onready var barracksPlacementOutline_Green = $BarracksPlacementOutline_Green
@onready var barracksPlacementOutline_Red = $BarracksPlacementOutline_Red

func _on_body_shape_entered(body_rid, body, body_shape_index, local_shape_index):
	var collidedTiles = []
	collidedTiles = body.get_used_cells(0)
	
	for collidedTile in collidedTiles:
		var collidedTileID = collidedTile.get_cell_source_id(0)
		
		if not collidedTileID == collidedTileID.get_cell_source_id(0):
			barracksPlacementOutline_Green.visible = false
			barracksPlacementOutline_Red.visible = true
		elif collidedTileID == collidedTileID.get_cell_source_id(0):
			barracksPlacementOutline_Green.visible = true
			barracksPlacementOutline_Red.visible = false

This is obviously wrong, but it should at least help people understand where my line of thinking is with this, I swear they make it far too silly to simply grab a tile ID unless I'm missing something obvious, which is why I'm wondering if custom data layers aren't the way to go because then I could grab a boolean for each tile and be done with it.

Invalid call. Nonexistent function 'get_cell_source_id' in base 'Vector2i'.

  • xyz replied to this.

    Lethn get_used_cells() returns an array of tile cell coordinates. You can use that coordinates as arguments to get_cell_tile_data() which will return TileData object for a given cell.

    You gave me some ideas with your explanation thanks, I went back to experimenting with get_cell_source_id again and it's sort of working, but of course ideally I want to be able to know if there's a different cell ID within the array or a null one I want to have it changed.

    extends Area2D
    
    @onready var barracksPlacementOutline_Green = $BarracksPlacementOutline_Green
    @onready var barracksPlacementOutline_Red = $BarracksPlacementOutline_Red
    
    func _on_body_shape_entered(body_rid, body, body_shape_index, local_shape_index):
    	var collidedTiles = []
    	collidedTiles = body.get_used_cells(0)
    	
    	for collidedTile in collidedTiles:
    		var tileIDData = body.get_cell_source_id(0, collidedTile)
    		
    		var tileIDs = []
    		tileIDs.append(tileIDData)
    		
    		for tileID in tileIDs:
    			if tileID == 0:
    				barracksPlacementOutline_Green.visible = true
    				barracksPlacementOutline_Red.visible = false
    			elif not tileID == 0:
    				barracksPlacementOutline_Green.visible = false
    				barracksPlacementOutline_Red.visible = true

    Definitely having a break now lol.

    My thinking with this code is by all accounts it should go through the new array I've created with the tile ID information rather than a generic return of the tile position, this then allows me to cycle through the array and run a check on that and if the area isn't overlapping with the correct ID tiles have my build placement turn red. I've clearly missed something out though in my steps so this is another thinking session for me.

    Yeah, did some print checking and it's not returning the tiles as I'm expecting so more tweaking, I think it's because I'm getting a fresh tilemap, so it's just blindly grabbing the whole tilemap now, the collidedTile won't return the cell ID and gives me a Vector2i error again.

    ............. REEEEEEEEEEEEEEEEEEEEEEEEE

    Umm I'm not sure I understand what exactly is the problem here. Is it figuring out the cells that are inside the area or is it doing something specific to those cells?

    Figuring out the cells that are inside the area, but I THINK this tutorial answers that question, I'm not going to mark the best answer until I've got a proper code example and I know it works.

    • xyz replied to this.

      Lethn but I THINK this tutorial answers that question

      I don't think so. Looks like it just shows how to set up custom tile data layer... although I just skimmed through the video and could have missed it.

      If you want to get a list of tiles that are inside some world space rect, simply transform that rect into tilemap local space and then into map space using TileMap::local_to_map(). That way you'll get min/max x/y ranges of all tiles that are inside the rect. Now just iterate through those ranges and do whatever you need with each cell.

      The problem is I have been barely able to find examples of this technique in most videos I've found on using tilemaps, that data layer is the closest thing I've found that's recent and there's barely any documentation on it, would you mind posting an example up? It's been an absolute nightmare working this out blind lol. I do not like the syntax that they've given for grabbing data from the TileMap cells.

      • xyz replied to this.

        Lethn
        Here's a method that returns map space rect given a global rect as an input:

        extends TileMap
        
        func global_rect_to_map(rect: Rect2) -> Rect2i:
        	var rect_map: Rect2i
        	rect_map.position = local_to_map(to_local(rect.position))
        	rect_map.size = local_to_map(to_local(rect.position + rect.size)) - rect_map.position + Vector2i(1, 1)
        	return rect_map

        And here's how you can get the global rect of a rectangle collider parented to area2d:

        var area_rect_global: Rect2
        area_rect_global.position = $area.to_global($area/shape.position - $area/shape.shape.size * .5)
        area_rect_global.size = $area.to_global($area/shape.position + $area/shape.shape.size * .5) - area_rect_global.position

        Example usage (setting all tilemap cells that touch the area rect):

        for j in area_rect_global.size.y:
        	for i in area_rect_global.size.x:
        		var v = area_rect_global.position + Vector2i(i,j)
        		$tilemap.set_cell(0, v, 0, Vector2i(0,0))	

        Oh for crying out loud I would have never been able to work that out on my own thanks lol, will look through it when I've got the time. There just isn't enough up to date examples on things like this for Godot 4, especially since the forum migration there was a lot of dead links I was coming across.