Using TileMapLayer, how can I make NavigationAgent2D avoid only the collision pollygons of my tiles in Godot 4.3?
Hello, I created an enemy and added NavigationAgent2D to make it follow my player, and enable Navigation for my background TileMapLayer (essentially grass).
I’m trying to make it avoid obstacles (such as trees and other objects), but my script does not detect the tiles I want it to avoid.
I have a script for my TileMapLayer to detect tiles to avoid.
extends TileMapLayer
@onready var decor_1: TileMapLayer = $"../Forest/Decor1"
func _use_tile_data_runtime_update(coords: Vector2i) -> bool:
if coords in decor_1.get_used_cells_by_id(0):
return true
return false
func _tile_data_runtime_update(coords: Vector2i, tile_data: TileData) -> void:
if coords in decor_1.get_used_cells_by_id(0):
tile_data.set_navigation_polygon(0, null)
But as you can see, only one tile is ignored for each of my elements.
I created one big tile for each element I placed on the map, as you can see, but only the framed cell (like in the screenshot) is avoided.
However, I want it to avoid only the collision part of my element because there is y-sorting.
How to make the collision polygon avoided by the navigation ?
You might substitute the if coords in decor_1.get_used_cells_by_id(0): with a for loop and add a custom offset for each cell
Alternatively, you could chech for collision for each cell with PhysicsDirectSpaceState2D.intersect_point() but it might have an impact on the performance
EDIT
With custom offset I mean the offset between the center tile and the one with collision shape
You should then check on runtime if a cell is occupied or not by looking at it
Ok
You might try to create a debug scene and test intersect_point() on various subjects to see if you can make it to work
Right now I can’t use my computer so I can’t actually do it
Also do this space_state.intersect_point(pointQuery, 1) instead of this space_state.intersect_point(pointQuery) to save some performance, but maybe it’s better to do it after you made it work
extends TileMapLayer
@onready var decor_1: TileMapLayer = $"../Forest/Decor1"
func _use_tile_data_runtime_update(coords: Vector2i) -> bool:
return intersect_point(map_to_local(coords))
func _tile_data_runtime_update(coords: Vector2i, tile_data: TileData) -> void:
if intersect_point(map_to_local(coords)):
tile_data.set_navigation_polygon(0, null)
func intersect_point(point: Vector2) -> bool:
var space_state := get_world_2d().direct_space_state
var pointQuery := PhysicsPointQueryParameters2D.new()
pointQuery.collide_with_areas = false
pointQuery.collision_mask = 2#<-HERE
pointQuery.position = point
var result = space_state.intersect_point(pointQuery, 1)
return not result.is_empty()
func _ready() -> void:
await get_tree().process_frame
notify_runtime_tile_data_update.call_deferred()
The thing is that the collision are not loaded yet when the TileMap gets updated so doing it manually after one frame worked! (for me at least)
To avoid scanning other collisions, I made it check only for collision mask 2 so to make this script work you have to add the decor_1 layer to collision mask 2
If you wish to use a different layer, you can get the int value for that mask using the following formula: 2**(desired_mask-1)
and use that value instead of the 2 where I signed it
I wish this will work for you too!