Issues with setting tiles as "impassable" for AStargrid2d

Godot Version

4.2

Question

I am having issues with setting a tile in a AStargrid2d as “impassable”.

I am using the astargrid2d.set_point_solid(position, false) to set the point to not be included in navigation, but when navigating, the path returned is still including the impassable tiles.

Any help is appreciated!

My code is two pieces:

The setup of the grid:

extends Node
var grid_size: Vector2
var navigation_grid: AStarGrid2D = AStarGrid2D.new()
var cell_size: Vector2i = Vector2i(32,32)
var half_cell: Vector2i = Vector2i(16,16)
var current_tilemap: TileMap
var directions: Array[Vector2i] = [Vector2i.RIGHT, Vector2i.LEFT, Vector2i.UP, Vector2i.DOWN]


func _ready():
	pass


## Setup of the size of the grid and the region of the grid based on the given TileMap
## Also updates the grid if there are changes (is_dirty = true)
func setup_astar_grid(map_tilemap: TileMap):
	current_tilemap = map_tilemap
	var map_tileset: TileSet = map_tilemap.tile_set
	cell_size = map_tileset.tile_size
	half_cell = map_tileset.tile_size / 2

	navigation_grid.cell_size = map_tileset.tile_size
	navigation_grid.region = map_tilemap.get_used_rect()
	navigation_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER

	navigation_grid.update()

	navigation_grid.set_point_solid(Vector2i(0,1), false)
	for layer in map_tilemap.get_layers_count():
		configure_walkale_tiles(layer)

	navigation_grid.update()

And then the actual function which parses all tilemap locations:

func configure_walkale_tiles(tilemap_layer: int):
	for x in current_tilemap.get_used_rect().size.x:
		for y in current_tilemap.get_used_rect().size.y:
			var x_offset = current_tilemap.get_used_rect().position.x
			var y_offset = current_tilemap.get_used_rect().position.y
			var tile_position: Vector2i = Vector2i(x + x_offset,y + y_offset)
			var tile_data = current_tilemap.get_cell_tile_data(tilemap_layer,tile_position)
			if tile_data == null:
				continue
			if tile_data.get_custom_data("is_walkable") == false:
				navigation_grid.set_point_solid(tile_position, false)

Let me know if anyone has experience with this!

This will depend on how you are using the nav grid, so post the code that moves an object using the grid.

Sure, this is also primarily three pieces

The logic which fires when a location is selected to move to:

## Get the path to the selected location on the grid and begin movement
func begin_unit_movement(move_start: Vector2i, move_target: Vector2i):
	optimal_path = Grid.get_path_from_grid(move_start, move_target)

	# Path is outside unit range
	if len(optimal_path) > movement_range:
		return

	destination = optimal_path.back()
	self.curve = Grid.get_path_curve(optimal_path)
	current_state = UnitState.MOVING

The logic which fetches the path from the astargrid2d, and the logic which sets the path2d nodes for the unit to follow:

## Gets the optimal A* path from the current to the target tile given two Vector2i's
func get_path_from_grid(current_tile: Vector2i, target_tile: Vector2i):
	return navigation_grid.get_id_path(current_tile, target_tile)
## From an optimal path, returns a Curve2d with the requisite points
func get_path_curve(optimal_path: Array[Vector2i]) -> Curve2D:
	var path_curve: Curve2D = Curve2D.new()
	for position in optimal_path:
		path_curve.add_point(navigation_grid.get_point_position(position))
	
	return path_curve

And then finally, moving the unit occurs by just moving it along the path2d

func _physics_process(delta):
	if current_state == UnitState.IDLE or current_state == UnitState.AWAITING or current_state == UnitState.TURN_FINISHED or current_state == UnitState.CAPTURING:
		unit_idle()
		return
	
	if unit.progress_ratio < 1 and current_state == UnitState.MOVING:
		unit_move(delta)
	
	if unit.progress_ratio >= 1 and current_state == UnitState.MOVING:
		await_command()

## Unit movement behavior while in the movement state
func unit_move(delta: float) -> void:
	unit.progress += movement_speed * delta
	animation_player.play("walk_seperated")

My assumption is that by setting a node’s solid to false, I am essentially saying “Do not use this in any pathfinding operations”, which means that when I fetch the path from the astargrid2d, the “optimal_path” should exclude the nodes specified as not solid.

That should be correct, so what is in Grid.get_path_from_grid()? How is that implemented?

It’s another helper function pointed to a manager:

## Gets the optimal A* path from the current to the target tile given two Vector2i's
func get_path_from_grid(current_tile: Vector2i, target_tile: Vector2i):
	return navigation_grid.get_id_path(current_tile, target_tile)

But all it’s doing is calling get_id_path(), I think at one point, I was doing another operation to convert, but am no longer doing that, so it may be unnecessary now.

Are begin_unit_movement() or set_point_solid() ever called asynchronously? like, from the physics thread or something? I am running out of ideas!

Hmm, begin_unit_movement is always called on a conditional for clicking on the screen essentially:

## Handles the selection action (mouse click on the screen)
func handle_select():
	var unit_tile: Vector2i = Grid.vector2_to_grid(unit.global_position)
	var mouse_tile: Vector2i = Grid.vector2_to_grid(get_global_mouse_position())
	var this_unit_clicked: bool = unit_tile == mouse_tile
	print("Mouse click at tile: ", mouse_tile, "Coordinate: ", get_global_mouse_position())
	
	if current_state == UnitState.CAPTURING:
		select_tile_to_capture(mouse_tile)
		return

	if !this_unit_clicked and !is_selected:
		return
	
	if !this_unit_clicked and is_selected:
		if current_state == UnitState.IDLE:
			begin_unit_movement(unit_tile, mouse_tile)
		else:
			deselect_unit()

	if this_unit_clicked and !is_selected:
		select_unit()

set_point_solid() is part of my setup function in my grid, which is called on level start by the level script

class_name Map extends Node2D

@export var tile_map: TileMap

# Called when the node enters the scene tree for the first time.

func _ready():

Grid.setup_astar_grid(tile_map)

# Called every frame. 'delta' is the elapsed time since the previous frame.

func _process(_delta):

pass

So setup_astar_grid() should only be called on level start.

The only thing I can think of is like, something isn’t being passed properly when calling “setup_astar_grid()”, and thus, the tile_map doesn’t actually exist properly, but everything else navigation wise seems to work thus far.

EDIT: handle_select() is basically called as part of _Input()

func _input(event):
	if event.is_action_pressed("select"):
		if current_state == UnitState.TURN_FINISHED or current_state == UnitState.AWAITING:
			return
		
		if current_state == UnitState.CONFIRMING:
			capture_tiles(selected_tile)
		handle_select()

Make sure you are getting some tile_data by adding prints there for that and the get_custom_data() call.
I don’t see anything wrong with the path-to-movement pipeline.

Gotcha, I added some debugging to check the tile status is marked correctly:

## Mark unwalkable tiles as "solid" excluding them from A* calculations
func configure_walkale_tiles(tilemap_layer: int):
	for x in current_tilemap.get_used_rect().size.x:
		for y in current_tilemap.get_used_rect().size.y:
			var x_offset = current_tilemap.get_used_rect().position.x
			var y_offset = current_tilemap.get_used_rect().position.y
			var tile_position: Vector2i = Vector2i(x + x_offset,y + y_offset)
			var tile_data = current_tilemap.get_cell_tile_data(tilemap_layer,tile_position)
			
			if tile_data == null:
				continue
			var is_tile_walkable: bool = tile_data.get_custom_data("is_walkable")
			if(is_tile_walkable == false):
				print("Tile at pos: ", tile_position, "walkable status is: ", is_tile_walkable)
			if is_tile_walkable == false:
				navigation_grid.set_point_solid(tile_position, false)

It looks like the status is correct, whether the individual positions on the grid are correct it seems to be as well?

I did some math and drew some placeholders based on the output tile positions (took the positions and multiplied by the cell_size (32), but here’s some output positions where “walkable” is supposedly false:

Tile at pos: (-11, -11)walkable status is: false
Tile at pos: (-10, -11)walkable status is: false
Tile at pos: (-9, -11)walkable status is: false
Tile at pos: (-8, -5)walkable status is: false
Tile at pos: (-8, -2)walkable status is: false
Tile at pos: (-8, -1)walkable status is: false
Tile at pos: (-8, 0)walkable status is: false
Tile at pos: (-8, 3)walkable status is: false
Tile at pos: (-8, 4)walkable status is: false
Tile at pos: (-6, -3)walkable status is: false
Tile at pos: (-5, -3)walkable status is: false
Tile at pos: (-5, -2)walkable status is: false
Tile at pos: (-5, -1)walkable status is: false
Tile at pos: (-4, -3)walkable status is: false
Tile at pos: (-4, -2)walkable status is: false
Tile at pos: (-4, -1)walkable status is: false
Tile at pos: (-3, -3)walkable status is: false
Tile at pos: (-2, -12)walkable status is: false
Tile at pos: (-2, -11)walkable status is: false
Tile at pos: (-2, -10)walkable status is: false
Tile at pos: (-2, -9)walkable status is: false
Tile at pos: (-2, -8)walkable status is: false
Tile at pos: (-1, -13)walkable status is: false
Tile at pos: (0, -13)walkable status is: false
Tile at pos: (0, -12)walkable status is: false
Tile at pos: (0, -10)walkable status is: false
Tile at pos: (0, -9)walkable status is: false
Tile at pos: (0, -8)walkable status is: false

And here’s a screenshot of the drawn tiles:

But if you try and move across one, it still moves across it regardless:
image

There’s nothing else I can think about. If you want to post your project I’ll take a look at it and test, but I have no idea what it could be besides a bug somewhere in the godot code.

No worries, thanks for your help! I will see what else I can do, maybe I’ll try setting everything to solid/not solid, and checking to see if that makes any difference haha

I wonder if I can get all solid tiles somehow, I’d have to see if there’s an accessor for that.

Try changing the traversal cost. It is not a wall, but it may tell you if it’s working or flat out getting a blank grid somehow.

I FIGURED IT OUT.

The issue is twofold:

  1. Calling update at ANY point after the setting of solid points will erase all of them:
void update()

Updates the internal state of the grid according to the parameters to prepare it to search the path. Needs to be called if parameters like region, cell_size or offset are changed. is_dirty() will return true if this is the case and this needs to be called.

Note: All point data (solidity and weight scale) will be cleared.
  1. When calling set_point_solid(pos,false) you actually don’t need to specify a second parameter, because the default is “true” and it sets the point to solid.

Honestly though, the documentation is very confusing about all this, because:

  1. It makes it sound like the default “true/false” for solid is “true”, not “false”
void set_point_solid(id: Vector2i, solid: bool = true)

Disables or enables the specified point for pathfinding. Useful for making an obstacle. By default, all points are enabled.

Note: Calling update() is not needed after the call of this function.
  1. it claims that calling update() is “not required” after the call to set_point_solid(), but it will actually break this call entirely.

Anyways, I have fixed my situation, by simply flipping around the set_point_solid(pos,true) rather than false (although will likely just remove the 2nd optional parameter), and then I removed my update() call following the set_point_solid calls.

Thanks for your help by the way!

1 Like

Wow, yeah, the documentation made me think it worked way differently too! This should be reported in the github. It should be easy to amend the docs.

1 Like

Yeah, I’ll see if I can figure out the process for reporting issues in the docs and see if that can be amended!

Thanks again!

2 Likes