Merge pull request #861 from dalexeev/astar-grid-2d

Rework "Grid-based Navigation with Astar" demo
This commit is contained in:
Rémi Verschelde
2023-02-28 17:35:29 +01:00
committed by GitHub
7 changed files with 109 additions and 215 deletions

View File

@@ -1,11 +1,11 @@
# Grid-based Navigation with Astar
# Grid-based Navigation with AStarGrid2D
This is an example of using AStar for navigation in 2D,
This is an example of using AStarGrid2D for navigation in 2D,
complete with Steering Behaviors in order to smooth the movement out.
Language: GDScript
Renderer: GLES 2
Renderer: Compatibility
Check out this demo on the asset library: https://godotengine.org/asset-library/asset/519

View File

@@ -1,61 +1,65 @@
extends Node2D
enum States { IDLE, FOLLOW }
enum State { IDLE, FOLLOW }
const MASS = 10.0
const ARRIVE_DISTANCE = 10.0
@export var speed: float = 200.0
var _state = States.IDLE
var _path = []
var _target_point_world = Vector2()
var _target_position = Vector2()
var _state = State.IDLE
var _velocity = Vector2()
@onready var _tile_map = $"../TileMap"
var _click_position = Vector2()
var _path = PackedVector2Array()
var _next_point = Vector2()
func _ready():
_change_state(States.IDLE)
_change_state(State.IDLE)
func _process(_delta):
if _state != States.FOLLOW:
if _state != State.FOLLOW:
return
var _arrived_to_next_point = _move_to(_target_point_world)
if _arrived_to_next_point:
var arrived_to_next_point = _move_to(_next_point)
if arrived_to_next_point:
_path.remove_at(0)
if len(_path) == 0:
_change_state(States.IDLE)
if _path.is_empty():
_change_state(State.IDLE)
return
_target_point_world = _path[0]
_next_point = _path[0]
func _unhandled_input(event):
if event.is_action_pressed("click"):
var global_mouse_pos = get_global_mouse_position()
if Input.is_key_pressed(KEY_SHIFT) and get_parent().get_node("TileMap").check_start_position(global_mouse_pos):
global_position = global_mouse_pos
else:
_target_position = global_mouse_pos
_change_state(States.FOLLOW)
_click_position = get_global_mouse_position()
if _tile_map.is_point_walkable(_click_position):
if event.is_action_pressed(&"teleport_to", false, true):
_change_state(State.IDLE)
global_position = _tile_map.round_local_position(_click_position)
elif event.is_action_pressed(&"move_to"):
_change_state(State.FOLLOW)
func _move_to(world_position):
var desired_velocity = (world_position - position).normalized() * speed
func _move_to(local_position):
var desired_velocity = (local_position - position).normalized() * speed
var steering = desired_velocity - _velocity
_velocity += steering / MASS
position += _velocity * get_process_delta_time()
rotation = _velocity.angle()
return position.distance_to(world_position) < ARRIVE_DISTANCE
return position.distance_to(local_position) < ARRIVE_DISTANCE
func _change_state(new_state):
if new_state == States.FOLLOW:
_path = get_parent().get_node(^"TileMap").get_astar_path(position, _target_position)
if _path.is_empty() or len(_path) == 1:
_change_state(States.IDLE)
if new_state == State.IDLE:
_tile_map.clear_path()
elif new_state == State.FOLLOW:
_path = _tile_map.find_path(position, _click_position)
if _path.size() < 2:
_change_state(State.IDLE)
return
# The index 0 is the starting cell.
# We don't want the character to move back to it in this example.
_target_point_world = _path[1]
_next_point = _path[1]
_state = new_state

File diff suppressed because one or more lines are too long

View File

@@ -1,177 +1,83 @@
extends TileMap
enum Tile { OBSTACLE, START_POINT, END_POINT }
const CELL_SIZE = Vector2(64, 64)
const BASE_LINE_WIDTH = 3.0
const DRAW_COLOR = Color.WHITE
# The Tilemap node doesn't have clear bounds so we're defining the map's limits here.
@export var map_size: Vector2i = Vector2.ONE * 18
# The object for pathfinding on 2D grids.
var _astar = AStarGrid2D.new()
var _map_rect = Rect2i()
# The path start and end variables use setter methods, defined below the initial values.
var path_start_position = Vector2i():
set(value):
if value in obstacles:
return
if is_outside_map_bounds(value):
return
set_cell(0, path_start_position, -1)
set_cell(0, value, 1, Vector2i())
path_start_position = value
if path_end_position and path_end_position != path_start_position:
_recalculate_path()
var path_end_position = Vector2i():
set(value):
if value in obstacles:
return
if is_outside_map_bounds(value):
return
set_cell(0, path_start_position, -1)
set_cell(0, value, 2, Vector2i())
path_end_position = value
if path_start_position != value:
_recalculate_path()
var _point_path = []
# You can only create an AStar node from code, not from the Scene tab.
@onready var astar_node = AStar3D.new()
# get_used_cells_by_id is a method from the TileMap node.
# Here the id 0 corresponds to the grey tile, the obstacles.
@onready var obstacles = get_used_cells(0)
var _start_point = Vector2i()
var _end_point = Vector2i()
var _path = PackedVector2Array()
func _ready():
var walkable_cells_list = astar_add_walkable_cells(obstacles)
astar_connect_walkable_cells(walkable_cells_list)
# Let's assume that the entire map is located at non-negative coordinates.
var map_size = get_used_rect().end
_map_rect = Rect2i(Vector2i(), map_size)
_astar.size = map_size
_astar.cell_size = CELL_SIZE
_astar.offset = CELL_SIZE * 0.5
_astar.default_compute_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
_astar.default_estimate_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
_astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
_astar.update()
for i in map_size.x:
for j in map_size.y:
var pos = Vector2i(i, j)
if get_cell_source_id(0, pos) == Tile.OBSTACLE:
_astar.set_point_solid(pos)
func _draw():
if _point_path.is_empty():
if _path.is_empty():
return
var point_start = _point_path[0]
var point_end = _point_path[len(_point_path) - 1]
set_cell(0, Vector2i(point_start.x, point_start.y), 1, Vector2i())
set_cell(0, Vector2i(point_end.x, point_end.y), 2, Vector2i())
var last_point = map_to_local(Vector2i(point_start.x, point_start.y))
for index in range(1, len(_point_path)):
var current_point = map_to_local(Vector2i(_point_path[index].x, _point_path[index].y))
var last_point = _path[0]
for index in range(1, len(_path)):
var current_point = _path[index]
draw_line(last_point, current_point, DRAW_COLOR, BASE_LINE_WIDTH, true)
draw_circle(current_point, BASE_LINE_WIDTH * 2.0, DRAW_COLOR)
last_point = current_point
# Loops through all cells within the map's bounds and
# adds all points to the astar_node, except the obstacles.
func astar_add_walkable_cells(obstacle_list = []):
var points_array = []
for y in range(map_size.y):
for x in range(map_size.x):
var point = Vector2i(x, y)
if point in obstacle_list:
continue
points_array.append(point)
# The AStar class references points with indices.
# Using a function to calculate the index from a point's coordinates
# ensures we always get the same index with the same input point.
var point_index = calculate_point_index(point)
# AStar works for both 2d and 3d, so we have to convert the point
# coordinates from and to Vector3s.
astar_node.add_point(point_index, Vector3(point.x, point.y, 0.0))
return points_array
func round_local_position(local_position):
return map_to_local(local_to_map(local_position))
# Once you added all points to the AStar node, you've got to connect them.
# The points don't have to be on a grid: you can use this class
# to create walkable graphs however you'd like.
# It's a little harder to code at first, but works for 2d, 3d,
# orthogonal grids, hex grids, tower defense games...
func astar_connect_walkable_cells(points_array):
for point in points_array:
var point_index = calculate_point_index(point)
# For every cell in the map, we check the one to the top, right.
# left and bottom of it. If it's in the map and not an obstalce.
# We connect the current point with it.
var points_relative = PackedVector2Array([
point + Vector2i.RIGHT,
point + Vector2i.LEFT,
point + Vector2i.DOWN,
point + Vector2i.UP,
])
for point_relative in points_relative:
var point_relative_index = calculate_point_index(point_relative)
if is_outside_map_bounds(point_relative):
continue
if not astar_node.has_point(point_relative_index):
continue
# Note the 3rd argument. It tells the astar_node that we want the
# connection to be bilateral: from point A to B and B to A.
# If you set this value to false, it becomes a one-way path.
# As we loop through all points we can set it to false.
astar_node.connect_points(point_index, point_relative_index, false)
func is_point_walkable(local_position):
var map_position = local_to_map(local_position)
if _map_rect.has_point(map_position):
return not _astar.is_point_solid(map_position)
return false
# This is a variation of the method above.
# It connects cells horizontally, vertically AND diagonally.
func astar_connect_walkable_cells_diagonal(points_array):
for point in points_array:
var point_index = calculate_point_index(point)
for local_y in range(3):
for local_x in range(3):
var point_relative = Vector2i(point.x + local_x - 1, point.y + local_y - 1)
var point_relative_index = calculate_point_index(point_relative)
if point_relative == point or is_outside_map_bounds(point_relative):
continue
if not astar_node.has_point(point_relative_index):
continue
astar_node.connect_points(point_index, point_relative_index, true)
func clear_path():
if not _path.is_empty():
_path.clear()
erase_cell(0, _start_point)
erase_cell(0, _end_point)
# Queue redraw to clear the lines and circles.
queue_redraw()
func calculate_point_index(point):
return point.x + map_size.x * point.y
func find_path(local_start_point, local_end_point):
clear_path()
_start_point = local_to_map(local_start_point)
_end_point = local_to_map(local_end_point)
_path = _astar.get_point_path(_start_point, _end_point)
func clear_previous_path_drawing():
if _point_path.is_empty():
return
var point_start = _point_path[0]
var point_end = _point_path[len(_point_path) - 1]
set_cell(0, Vector2i(point_start.x, point_start.y), -1)
set_cell(0, Vector2i(point_end.x, point_end.y), -1)
if not _path.is_empty():
set_cell(0, _start_point, Tile.START_POINT, Vector2i())
set_cell(0, _end_point, Tile.END_POINT, Vector2i())
func is_outside_map_bounds(point):
return point.x < 0 or point.y < 0 or point.x >= map_size.x or point.y >= map_size.y
func check_start_position(world_start):
var start_point = local_to_map(world_start)
if start_point in obstacles:
return false
return true
func get_astar_path(world_start, world_end):
self.path_start_position = local_to_map(world_start)
self.path_end_position = local_to_map(world_end)
_recalculate_path()
var path_world = []
for point in _point_path:
var point_world = map_to_local(Vector2i(point.x, point.y))
path_world.append(point_world)
return path_world
func _recalculate_path():
clear_previous_path_drawing()
var start_point_index = calculate_point_index(path_start_position)
var end_point_index = calculate_point_index(path_end_position)
# This method gives us an array of points. Note you need the start and
# end points' indices as input.
_point_path = astar_node.get_point_path(start_point_index, end_point_index)
# Redraw the lines and circles from the start to the end point.
queue_redraw()
return _path.duplicate()

View File

@@ -10,10 +10,10 @@ config_version=5
[application]
config/name="Grid-based Pathfinding with Astar"
config/description="This is an example of using AStar for navigation in 2D,
config/name="Grid-based Pathfinding with AStarGrid2D"
config/description="This is an example of using AStarGrid2D for navigation in 2D,
complete with Steering Behaviors in order to smooth the movement out."
run/main_scene="res://Game.tscn"
run/main_scene="res://game.tscn"
config/features=PackedStringArray("4.0")
config/icon="res://icon.png"
@@ -24,14 +24,17 @@ window/stretch/aspect="expand"
[input]
click={
move_to={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"pressed":false,"double_click":false,"script":null)
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"pressed":true,"double_click":false,"script":null)
]
}
teleport_to={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"pressed":true,"double_click":false,"script":null)
]
}
[rendering]
quality/driver/driver_name="GLES2"
vram_compression/import_etc=true
vram_compression/import_etc2=false
renderer/rendering_method="gl_compatibility"

View File

@@ -1,19 +0,0 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://sprites/obstacle.png" type="Texture2D" id=1]
[ext_resource path="res://sprites/path_start.png" type="Texture2D" id=2]
[ext_resource path="res://sprites/path_end.png" type="Texture2D" id=3]
[node name="Tileset" type="Node2D"]
[node name="Obstacle" type="Sprite2D" parent="."]
position = Vector2(32, 32)
texture = ExtResource( 1 )
[node name="PathStart" type="Sprite2D" parent="."]
position = Vector2(112, 32)
texture = ExtResource( 2 )
[node name="PathEnd" type="Sprite2D" parent="."]
position = Vector2(192, 32)
texture = ExtResource( 3 )