|
# Physics-collision-based navigation grid, a quick and simple alternative for navigation polygons. |
|
# Part of "Xenomusa" (C) Mara Huldra 2022 |
|
# SPDX-License-Identifier: MIT |
|
extends Node2D |
|
|
|
@export var tilemap: TileMap |
|
@export var excludes: Array[CollisionObject2D] |
|
@export_flags_2d_physics var nav_physics_layers: int = 1 |
|
|
|
var update_nav_grid: bool = false |
|
var nav_grid: AStarGrid2D |
|
var nav_tile_size: Vector2 |
|
|
|
|
|
func _ready() -> void: |
|
queue_nav_grid_update() |
|
|
|
|
|
func _partial_update(used_rect: Rect2i) -> void: |
|
var space_state := get_world_2d().direct_space_state |
|
|
|
# Collide with a circle (fastest). |
|
var shape := CircleShape2D.new() |
|
shape.radius = nav_tile_size.x / 2.0 |
|
|
|
var parameters := PhysicsShapeQueryParameters2D.new() |
|
parameters.collide_with_areas = true |
|
parameters.collide_with_bodies = true |
|
parameters.collision_mask = nav_physics_layers |
|
parameters.shape = shape |
|
var exclude_rids = [] |
|
for obj in excludes: |
|
exclude_rids.append(obj.get_rid()) |
|
parameters.exclude = exclude_rids |
|
|
|
for y in range(used_rect.position.y, used_rect.end.y): |
|
for x in range(used_rect.position.x, used_rect.end.x): |
|
var pos := Vector2i(x, y) |
|
|
|
# Position shape in middle, edge, or corner of tile (2x "oversampling"). |
|
parameters.transform.origin = Vector2(pos) * nav_tile_size |
|
var collisions := space_state.collide_shape(parameters, 1) |
|
var solid := collisions.size() > 0 |
|
|
|
nav_grid.set_point_solid(pos, solid) |
|
|
|
func _build_navigation_map() -> void: |
|
print("Updating nav grid.") |
|
nav_grid = AStarGrid2D.new() |
|
if tilemap == null: |
|
printerr("FoxNav: No tilemap installed") |
|
return |
|
|
|
# Tiles are twice the resolution of the tileset. |
|
var used_rect: Rect2i = tilemap.get_used_rect() |
|
used_rect.position *= 2 |
|
used_rect.size *= 2 |
|
nav_tile_size = Vector2(tilemap.tile_set.tile_size) / 2.0 |
|
|
|
nav_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES |
|
nav_grid.region = used_rect |
|
nav_grid.update() |
|
|
|
_partial_update(used_rect) |
|
|
|
func queue_nav_grid_update() -> void: |
|
# XXX needs a way to specify only a certain rectangle. |
|
update_nav_grid = true |
|
|
|
func navigate(source: Vector2, destination: Vector2) -> Array[Vector2]: |
|
var source_id = Vector2i(round(source / nav_tile_size)) |
|
var destination_id = Vector2i(round(destination / nav_tile_size)) |
|
|
|
var ret_path: Array[Vector2] = [] |
|
var first := true # Skip first item it will be the current cell center. |
|
for id in nav_grid.get_id_path(source_id, destination_id): |
|
if not first: |
|
ret_path.append(Vector2(id) * nav_tile_size) |
|
first = false |
|
|
|
return ret_path |
|
|
|
func _physics_process(_delta) -> void: |
|
if update_nav_grid: # Must be done in physics process. |
|
_build_navigation_map() |
|
update_nav_grid = false |