mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2026-01-04 23:10:08 +01:00
Merge pull request #861 from dalexeev/astar-grid-2d
Rework "Grid-based Navigation with Astar" demo
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 )
|
||||
Reference in New Issue
Block a user