A-Star Path Finding#
Explanation#
A-star path finding can be used to find a way to go from one spot to another around barriers. This example just draws the path, based on the coordinates returned. If you want the enemy to follow that path, see Sprites That Follow a Path.
Source#
1"""
2A-Star Path-finding
3
4Artwork from https://kenney.nl
5
6If Python and Arcade are installed, this example can be run from the command line with:
7python -m arcade.examples.astar_pathfinding
8"""
9
10from __future__ import annotations
11
12import arcade
13import random
14
15SPRITE_IMAGE_SIZE = 128
16SPRITE_SCALING = 0.25
17SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING)
18
19SCREEN_WIDTH = 800
20SCREEN_HEIGHT = 600
21SCREEN_TITLE = "A-Star Path-finding"
22
23MOVEMENT_SPEED = 5
24
25VIEWPORT_MARGIN = 100
26
27
28class MyGame(arcade.Window):
29 """
30 Main application class.
31 """
32
33 def __init__(self, width, height, title):
34 """
35 Initializer
36 """
37
38 # Call the parent class initializer
39 super().__init__(width, height, title)
40
41 # Variables that will hold sprite lists
42 self.player_list = None
43 self.wall_list = None
44 self.enemy_list = None
45
46 # Set up the player info
47 self.player = None
48
49 # Track the current state of what key is pressed
50 self.left_pressed = False
51 self.right_pressed = False
52 self.up_pressed = False
53 self.down_pressed = False
54
55 self.physics_engine = None
56
57 # --- Related to paths
58 # List of points that makes up a path between two points
59 self.path = None
60 # List of points we checked to see if there is a barrier there
61 self.barrier_list = None
62
63 # Used in scrolling
64 self.view_bottom = 0
65 self.view_left = 0
66
67 # Set the window background color
68 self.background_color = arcade.color.AMAZON
69
70 def setup(self):
71 """ Set up the game and initialize the variables. """
72
73 # Sprite lists
74 self.player_list = arcade.SpriteList()
75 self.wall_list = arcade.SpriteList(use_spatial_hash=True,
76 spatial_hash_cell_size=128)
77 self.enemy_list = arcade.SpriteList()
78
79 # Set up the player
80 resource = ":resources:images/animated_characters/" \
81 "female_person/femalePerson_idle.png"
82 self.player = arcade.Sprite(resource, scale=SPRITE_SCALING)
83 self.player.center_x = SPRITE_SIZE * 5
84 self.player.center_y = SPRITE_SIZE * 1
85 self.player_list.append(self.player)
86
87 # Set enemies
88 resource = ":resources:images/animated_characters/zombie/zombie_idle.png"
89 enemy = arcade.Sprite(resource, scale=SPRITE_SCALING)
90 enemy.center_x = SPRITE_SIZE * 4
91 enemy.center_y = SPRITE_SIZE * 7
92 self.enemy_list.append(enemy)
93
94 spacing = SPRITE_SIZE * 3
95 for column in range(10):
96 for row in range(15):
97 sprite = arcade.Sprite(":resources:images/tiles/grassCenter.png",
98 scale=SPRITE_SCALING)
99
100 x = (column + 1) * spacing
101 y = (row + 1) * sprite.height
102
103 sprite.center_x = x
104 sprite.center_y = y
105 if random.randrange(100) > 30:
106 self.wall_list.append(sprite)
107
108 self.physics_engine = arcade.PhysicsEngineSimple(self.player,
109 self.wall_list)
110
111 # --- Path related
112 # This variable holds the travel-path. We keep it as an attribute so
113 # we can calculate it in on_update, and draw it in on_draw.
114 self.path = None
115 # Grid size for calculations. The smaller the grid, the longer the time
116 # for calculations. Make sure the grid aligns with the sprite wall grid,
117 # or some openings might be missed.
118 grid_size = SPRITE_SIZE
119
120 # Calculate the playing field size. We can't generate paths outside of
121 # this.
122 playing_field_left_boundary = -SPRITE_SIZE * 2
123 playing_field_right_boundary = SPRITE_SIZE * 35
124 playing_field_top_boundary = SPRITE_SIZE * 17
125 playing_field_bottom_boundary = -SPRITE_SIZE * 2
126
127 # This calculates a list of barriers. By calculating it here in the
128 # init, we are assuming this list does not change. In this example,
129 # our walls don't move, so that is ok. If we want moving barriers (such as
130 # moving platforms or enemies) we need to recalculate. This can be an
131 # time-intensive process depending on the playing field size and grid
132 # resolution.
133
134 # Note: If the enemy sprites are the same size, we only need to calculate
135 # one of these. We do NOT need a different one for each enemy. The sprite
136 # is just used for a size calculation.
137 self.barrier_list = arcade.AStarBarrierList(enemy,
138 self.wall_list,
139 grid_size,
140 playing_field_left_boundary,
141 playing_field_right_boundary,
142 playing_field_bottom_boundary,
143 playing_field_top_boundary)
144
145 def on_draw(self):
146 """
147 Render the screen.
148 """
149 # This command has to happen before we start drawing
150 self.clear()
151
152 # Draw all the sprites.
153 self.player_list.draw()
154 self.wall_list.draw()
155 self.enemy_list.draw()
156
157 if self.path:
158 arcade.draw_line_strip(self.path, arcade.color.BLUE, 2)
159
160 def on_update(self, delta_time):
161 """ Movement and game logic """
162
163 # Calculate speed based on the keys pressed
164 self.player.change_x = 0
165 self.player.change_y = 0
166
167 if self.up_pressed and not self.down_pressed:
168 self.player.change_y = MOVEMENT_SPEED
169 elif self.down_pressed and not self.up_pressed:
170 self.player.change_y = -MOVEMENT_SPEED
171 if self.left_pressed and not self.right_pressed:
172 self.player.change_x = -MOVEMENT_SPEED
173 elif self.right_pressed and not self.left_pressed:
174 self.player.change_x = MOVEMENT_SPEED
175
176 # Update the character
177 self.physics_engine.update()
178
179 # Calculate a path to the player
180 enemy = self.enemy_list[0]
181 # Set to True if we can move diagonally. Note that diagonal movement
182 # might cause the enemy to clip corners.
183 self.path = arcade.astar_calculate_path(enemy.position,
184 self.player.position,
185 self.barrier_list,
186 diagonal_movement=False)
187 # print(self.path,"->", self.player.position)
188
189 # --- Manage Scrolling ---
190
191 # Keep track of if we changed the boundary. We don't want to call the
192 # set_viewport command if we didn't change the view port.
193 changed = False
194
195 # Scroll left
196 left_boundary = self.view_left + VIEWPORT_MARGIN
197 if self.player.left < left_boundary:
198 self.view_left -= left_boundary - self.player.left
199 changed = True
200
201 # Scroll right
202 right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN
203 if self.player.right > right_boundary:
204 self.view_left += self.player.right - right_boundary
205 changed = True
206
207 # Scroll up
208 top_boundary = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN
209 if self.player.top > top_boundary:
210 self.view_bottom += self.player.top - top_boundary
211 changed = True
212
213 # Scroll down
214 bottom_boundary = self.view_bottom + VIEWPORT_MARGIN
215 if self.player.bottom < bottom_boundary:
216 self.view_bottom -= bottom_boundary - self.player.bottom
217 changed = True
218
219 # Make sure our boundaries are integer values. While the view port does
220 # support floating point numbers, for this application we want every pixel
221 # in the view port to map directly onto a pixel on the screen. We don't want
222 # any rounding errors.
223 self.view_left = int(self.view_left)
224 self.view_bottom = int(self.view_bottom)
225
226 # If we changed the boundary values, update the view port to match
227 if changed:
228 arcade.set_viewport(self.view_left,
229 SCREEN_WIDTH + self.view_left,
230 self.view_bottom,
231 SCREEN_HEIGHT + self.view_bottom)
232
233 def on_key_press(self, key, modifiers):
234 """Called whenever a key is pressed. """
235
236 if key == arcade.key.UP:
237 self.up_pressed = True
238 elif key == arcade.key.DOWN:
239 self.down_pressed = True
240 elif key == arcade.key.LEFT:
241 self.left_pressed = True
242 elif key == arcade.key.RIGHT:
243 self.right_pressed = True
244
245 def on_key_release(self, key, modifiers):
246 """Called when the user releases a key. """
247
248 if key == arcade.key.UP:
249 self.up_pressed = False
250 elif key == arcade.key.DOWN:
251 self.down_pressed = False
252 elif key == arcade.key.LEFT:
253 self.left_pressed = False
254 elif key == arcade.key.RIGHT:
255 self.right_pressed = False
256
257
258def main():
259 """ Main function """
260 window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
261 window.setup()
262 arcade.run()
263
264
265if __name__ == "__main__":
266 main()