A-Star Path Finding#

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#

astar_pathfinding.py#
  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()