Browse Source

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

Rework "Grid-based Navigation with Astar" demo
Rémi Verschelde 2 years ago
parent
commit
e306a408dc

+ 3 - 3
2d/navigation_astar/README.md

@@ -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.
 complete with Steering Behaviors in order to smooth the movement out.
 
 
 Language: GDScript
 Language: GDScript
 
 
-Renderer: GLES 2
+Renderer: Compatibility
 
 
 Check out this demo on the asset library: https://godotengine.org/asset-library/asset/519
 Check out this demo on the asset library: https://godotengine.org/asset-library/asset/519
 
 

+ 32 - 28
2d/navigation_astar/character.gd

@@ -1,61 +1,65 @@
 extends Node2D
 extends Node2D
 
 
-enum States { IDLE, FOLLOW }
+enum State { IDLE, FOLLOW }
 
 
 const MASS = 10.0
 const MASS = 10.0
 const ARRIVE_DISTANCE = 10.0
 const ARRIVE_DISTANCE = 10.0
 
 
 @export var speed: float = 200.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()
 var _velocity = Vector2()
 
 
+@onready var _tile_map = $"../TileMap"
+
+var _click_position = Vector2()
+var _path = PackedVector2Array()
+var _next_point = Vector2()
+
 func _ready():
 func _ready():
-	_change_state(States.IDLE)
+	_change_state(State.IDLE)
 
 
 
 
 func _process(_delta):
 func _process(_delta):
-	if _state != States.FOLLOW:
+	if _state != State.FOLLOW:
 		return
 		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)
 		_path.remove_at(0)
-		if len(_path) == 0:
-			_change_state(States.IDLE)
+		if _path.is_empty():
+			_change_state(State.IDLE)
 			return
 			return
-		_target_point_world = _path[0]
+		_next_point = _path[0]
 
 
 
 
 func _unhandled_input(event):
 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
 	var steering = desired_velocity - _velocity
 	_velocity += steering / MASS
 	_velocity += steering / MASS
 	position += _velocity * get_process_delta_time()
 	position += _velocity * get_process_delta_time()
 	rotation = _velocity.angle()
 	rotation = _velocity.angle()
-	return position.distance_to(world_position) < ARRIVE_DISTANCE
+	return position.distance_to(local_position) < ARRIVE_DISTANCE
 
 
 
 
 func _change_state(new_state):
 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
 			return
 		# The index 0 is the starting cell.
 		# The index 0 is the starting cell.
 		# We don't want the character to move back to it in this example.
 		# 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
 	_state = new_state

File diff suppressed because it is too large
+ 6 - 6
2d/navigation_astar/game.tscn


+ 55 - 149
2d/navigation_astar/pathfind_astar.gd

@@ -1,177 +1,83 @@
 extends TileMap
 extends TileMap
 
 
+enum Tile { OBSTACLE, START_POINT, END_POINT }
+
+const CELL_SIZE = Vector2(64, 64)
 const BASE_LINE_WIDTH = 3.0
 const BASE_LINE_WIDTH = 3.0
 const DRAW_COLOR = Color.WHITE
 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 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)
+# The object for pathfinding on 2D grids.
+var _astar = AStarGrid2D.new()
+var _map_rect = Rect2i()
+
+var _start_point = Vector2i()
+var _end_point = Vector2i()
+var _path = PackedVector2Array()
 
 
 func _ready():
 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():
 func _draw():
-	if _point_path.is_empty():
+	if _path.is_empty():
 		return
 		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_line(last_point, current_point, DRAW_COLOR, BASE_LINE_WIDTH, true)
 		draw_circle(current_point, BASE_LINE_WIDTH * 2.0, DRAW_COLOR)
 		draw_circle(current_point, BASE_LINE_WIDTH * 2.0, DRAW_COLOR)
 		last_point = current_point
 		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
-
-
-# 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)
-
-
-# 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 calculate_point_index(point):
-	return point.x + map_size.x * point.y
-
-
-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)
+func round_local_position(local_position):
+	return map_to_local(local_to_map(local_position))
 
 
 
 
-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 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
 
 
 
 
-func check_start_position(world_start):
-	var start_point = local_to_map(world_start)
-	if start_point in obstacles:
-		return false
+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()
 
 
-	return true
 
 
+func find_path(local_start_point, local_end_point):
+	clear_path()
 
 
-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
+	_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)
 
 
+	if not _path.is_empty():
+		set_cell(0, _start_point, Tile.START_POINT, Vector2i())
+		set_cell(0, _end_point, Tile.END_POINT, Vector2i())
 
 
-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.
 	# Redraw the lines and circles from the start to the end point.
 	queue_redraw()
 	queue_redraw()
+
+	return _path.duplicate()

+ 11 - 8
2d/navigation_astar/project.godot

@@ -10,10 +10,10 @@ config_version=5
 
 
 [application]
 [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."
 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/features=PackedStringArray("4.0")
 config/icon="res://icon.png"
 config/icon="res://icon.png"
 
 
@@ -24,14 +24,17 @@ window/stretch/aspect="expand"
 
 
 [input]
 [input]
 
 
-click={
+move_to={
 "deadzone": 0.5,
 "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]
 [rendering]
 
 
-quality/driver/driver_name="GLES2"
-vram_compression/import_etc=true
-vram_compression/import_etc2=false
+renderer/rendering_method="gl_compatibility"

+ 0 - 0
2d/navigation_astar/tileset/tileset.tres → 2d/navigation_astar/tileset.tres


+ 0 - 19
2d/navigation_astar/tileset/tileset_source.tscn

@@ -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 )

Some files were not shown because too many files changed in this diff