What
For my current Project I needed Pathfinding for the enemies and tried a bunch of things. My goal was to get an enemie which followed a specific Path and to give a few RallyPoints which had to be reached in order. But the Player can block the Path by placing Walls between the RallyTargets.
Now I just want to summarize my findings for future reference.
Be aware that this will only be the basic Usage and nothing advanced.
There are quite a few ways for Pathfinding in Godot 4:
- NavigationAgent (Godot way of using AStar)
- with TileMaps
- per Mesh
- Astar2D (A* the hard way)
- AstarGrid2D (A* optimized for 2D Grids)
- PathFollow2D (simple way of path following, without pathfinding just straight from waypoint to waypoint)
NavigationAgent
with TileMaps
- create a new Scene with a
TileMapLayer
(map) and add a new TileSet- make necessary Settings, e.g. TileSize
- Physics Layers - Add Element
- Navigation Layers - Add Element
- add the Example Tilemap to the TileSet
- go to Paint - Physics Layer - and paint your walls (in the example the black block)
- go to Paint - Navigation Layer - and paint your floor (in the example the white block)
- create a small example level..
- create a new Scene with a
CharacterBody2D
(follower)- make necessary settings (so it isnt stuck on corners)
- Motion Mode: Floating
- Wall Min Slide Angle: 0
- remove collision between enemies (not necessary..)
- Collision Layer: 3
- Mask: nothing
- add a Sprite2D
- add a new AtlasTexture
- Edit Region - Select the Red Ball
- add a new AtlasTexture
- add a CollisionShape2D
- use a small circle shape (if its exactly the size of the sprite/rectangle it can be stuck on corners)
- add a NavigationAgent2D
- add a Script
- make necessary settings (so it isnt stuck on corners)
- create a new Scene with a
Area2D
(rally point)- now to create a target for the characterbody2d to charge at we could use our player or we create another target (which i needed for my project ..)
- repeat the same as with the follower (sprite and collision shape)
- go to Node - Groups - Add a new group and call it
RallyPoints
- Collision Settings
- Collision: Nothing
- Mask: 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
extends CharacterBody2D
const speed = 200
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var rally_points: Array[Area2D] = []
var current_target_index: int = 0
func _ready():
get_rally_targets()
calculate_path()
func _physics_process(delta: float) -> void:
move()
func get_rally_targets():
var rally_nodes = get_tree().get_nodes_in_group("RallyPoints")
for node in rally_nodes:
if node is Area2D:
rally_points.append(node)
rally_points.sort()
func calculate_path() -> void:
if current_target_index < rally_points.size():
nav_agent.target_position = rally_points[current_target_index].global_position
else:
print("FINAL TARGET REACHED")
self.queue_free()
func is_rally_point_reached() -> bool:
if current_target_index < rally_points.size():
var current_rally_point = rally_points[current_target_index]
return current_rally_point.overlaps_body(self)
return false
func move():
if is_rally_point_reached():
print(self, " RALLY POINT REACHED")
current_target_index += 1
calculate_path()
else:
var dir = to_local(nav_agent.get_next_path_position()).normalized()
velocity = dir * speed
move_and_slide()
- now add the follower and the rallytarget to the tilemap Scene
per Mesh
soon to be coming (surely…) …
… but pretty identical to the Tilemap variant
AstarGrid2D
- create a new Scene with a
Node2D
(map)- add a
TileMapLayer
(floor) and then a new TileSet- RightClick - Access as UniqueName
- add a
TileMapLayer
(walls) and then a new TileSet- RightClick - Access as UniqueName
- create a small example level..
- start at the coordinate (0,0), otherwise
- add a
- create a new Scene with a
CharacterBody2D
(follower)- make necessary settings (so it isnt stuck on corners)
- Motion Mode: Floating
- Wall Min Slide Angle: 0
- remove collision between enemies (not necessary..)
- Collision Layer: 3
- Mask: nothing
- add a Sprite2D
- add a new AtlasTexture
- Edit Region - Select the Red Ball
- add a new AtlasTexture
- add a CollisionShape2D
- use a small circle shape (if its exactly the size of the sprite/rectangle it can be stuck on corners)
- add a Script
- make necessary settings (so it isnt stuck on corners)
Script
- initate grid
- get target coords
- send self to target (+ draw line if necessary)
- check if target reached / last target reached => repeat from 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
extends CharacterBody2D
@onready var TileMapLayerFloor: TileMapLayer = %floor
@onready var TileMapLayerWalls: TileMapLayer = %walls
@onready var map: Node2D = get_parent()
const speed = 500
var astargrid = AStarGrid2D.new()
var rally_points: Array[Area2D] = []
var current_path: PackedVector2Array = []
var target_point_index = 0
var current_path_line: Line2D = null
func _ready():
initialize_astargrid2d()
get_rally_targets()
set_next_target()
func _process(_delta):
if Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE):
print_positions()
if !current_path.is_empty():
var next_point = current_path[0]
var direction = (next_point - global_position).normalized()
velocity = direction * speed
move_and_slide()
if global_position.distance_to(next_point) < 5:
current_path.remove_at(0)
if current_path.is_empty():
set_next_target()
func initialize_astargrid2d():
astargrid.region = TileMapLayerFloor.get_used_rect()
astargrid.cell_size = TileMapLayerFloor.tile_set.tile_size
astargrid.jumping_enabled = true
astargrid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES
astargrid.update()
for tile in TileMapLayerWalls.get_used_cells():
astargrid.set_point_solid(tile)
astargrid.update()
func get_rally_targets():
var rally_nodes = get_tree().get_nodes_in_group("RallyPoints")
for node in rally_nodes:
if node is Area2D:
rally_points.append(node)
rally_points.sort()
func set_next_target():
if target_point_index < rally_points.size():
var target = rally_points[target_point_index]
var start_point = TileMapLayerFloor.local_to_map(global_position)
var end_point = TileMapLayerFloor.local_to_map(target.global_position)
var path = astargrid.get_point_path(start_point, end_point)
if path.is_empty():
print("No path found to target ", target_point_index + 1)
target_point_index += 1
set_next_target()
else:
current_path.clear()
current_path = path
target_point_index += 1
print_path(path)
else:
print("All targets reached")
queue_free()
func print_path(path: PackedVector2Array) -> void:
if current_path_line:
current_path_line.queue_free()
var world_path: PackedVector2Array = []
for point:Vector2i in path:
world_path.append(point + Vector2i(astargrid.cell_size) / 2)
current_path_line = Line2D.new()
map.add_child(current_path_line)
current_path_line.width = 2
current_path_line.default_color = Color.LIGHT_GREEN
current_path_line.begin_cap_mode = Line2D.LINE_CAP_ROUND
current_path_line.end_cap_mode = Line2D.LINE_CAP_ROUND
current_path_line.joint_mode = Line2D.LINE_JOINT_ROUND
current_path_line.points = world_path
#-------------DEBUGGING-------------
func print_positions() -> void:
var global_pos = get_global_mouse_position()
var tilemap_pos_floor = TileMapLayerFloor.local_to_map(global_pos)
var in_bounds = astargrid.is_in_bounds(tilemap_pos_floor.x, tilemap_pos_floor.y)
var is_solid = astargrid.is_point_solid(tilemap_pos_floor)
print("get_tree:")
get_tree().root.print_tree()
print("Global Position: ", global_pos)
print("Map Position Floor: ", tilemap_pos_floor)
print("in_bounds: ", in_bounds)
print("is_solid: " , is_solid)
AStar2D
soon to be coming (surely…) …
… but pretty identical to the Tilemap variant
PathFollow2D
soon to be coming (surely…) …
Afterwords
Here the zip File for the project:
Have Fun!
Comments
Comment on Github