Work with levels and a tiled map

Screenshot of using a larger map created by the Tiled Map Editor
sprite_tiled_map_with_levels.py
  1"""
  2Load a Tiled map file with Levels
  3
  4Artwork from: https://kenney.nl
  5Tiled available from: https://www.mapeditor.org/
  6
  7If Python and Arcade are installed, this example can be run from the command line with:
  8python -m arcade.examples.sprite_tiled_map_with_levels
  9"""
 10import time
 11
 12import arcade
 13
 14TILE_SPRITE_SCALING = 0.5
 15PLAYER_SCALING = 0.6
 16
 17SCREEN_WIDTH = 800
 18SCREEN_HEIGHT = 600
 19SCREEN_TITLE = "Sprite Tiled Map with Levels Example"
 20SPRITE_PIXEL_SIZE = 128
 21GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SPRITE_SCALING
 22
 23# How many pixels to keep as a minimum margin between the character
 24# and the edge of the screen.
 25VIEWPORT_MARGIN_TOP = 60
 26VIEWPORT_MARGIN_BOTTOM = 60
 27VIEWPORT_MARGIN_RIGHT = 270
 28VIEWPORT_MARGIN_LEFT = 270
 29
 30# Physics
 31MOVEMENT_SPEED = 5
 32JUMP_SPEED = 23
 33GRAVITY = 1.1
 34
 35
 36class MyGame(arcade.Window):
 37    """Main application class."""
 38
 39    def __init__(self):
 40        """
 41        Initializer
 42        """
 43        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 44
 45        # Tilemap Object
 46        self.tile_map = None
 47
 48        # Sprite lists
 49        self.player_list = None
 50
 51        # Set up the player
 52        self.score = 0
 53        self.player_sprite = None
 54
 55        self.physics_engine = None
 56        self.cam = None
 57        self.end_of_map = 0
 58        self.game_over = False
 59        self.last_time = None
 60        self.frame_count = 0
 61        self.fps_message = None
 62
 63        self.level = 1
 64        self.max_level = 2
 65
 66    def setup(self):
 67        """Set up the game and initialize the variables."""
 68
 69        # Sprite lists
 70        self.player_list = arcade.SpriteList()
 71
 72        # Set up the player
 73        self.player_sprite = arcade.Sprite(
 74            ":resources:images/animated_characters/female_person/femalePerson_idle.png",
 75            scale=PLAYER_SCALING,
 76        )
 77
 78        # Starting position of the player
 79        self.player_sprite.center_x = 128
 80        self.player_sprite.center_y = 64
 81        self.player_list.append(self.player_sprite)
 82
 83        self.load_level(self.level)
 84
 85        self.game_over = False
 86
 87    def load_level(self, level):
 88        # layer_options = {"Platforms": {"use_spatial_hash": True}}
 89
 90        # Read in the tiled map
 91        self.tile_map = arcade.load_tilemap(
 92            f":resources:tiled_maps/level_{level}.json", scaling=TILE_SPRITE_SCALING
 93        )
 94
 95        # --- Walls ---
 96
 97        # Calculate the right edge of the my_map in pixels
 98        self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
 99
100        self.physics_engine = arcade.PhysicsEnginePlatformer(
101            self.player_sprite,
102            self.tile_map.sprite_lists["Platforms"],
103            gravity_constant=GRAVITY,
104        )
105
106        # --- Other stuff
107        # Set the background color
108        if self.tile_map.background_color:
109            self.background_color = self.tile_map.background_color
110
111        # Reset cam
112        self.cam = arcade.camera.Camera2D()
113
114    def on_draw(self):
115        """
116        Render the screen.
117        """
118
119        self.frame_count += 1
120
121        # This command has to happen before we start drawing
122        self.clear()
123
124        # Draw all the sprites.
125        self.player_list.draw()
126        self.tile_map.sprite_lists["Platforms"].draw()
127
128        if self.last_time and self.frame_count % 60 == 0:
129            fps = 1.0 / (time.time() - self.last_time) * 60
130            self.fps_message = f"FPS: {fps:5.0f}"
131
132        if self.fps_message:
133            arcade.draw_text(
134                self.fps_message,
135                self.cam.left + 10,
136                self.cam.bottom + 40,
137                arcade.color.BLACK,
138                14,
139            )
140
141        if self.frame_count % 60 == 0:
142            self.last_time = time.time()
143
144        # Put the text on the screen.
145        # Adjust the text position based on the view port so that we don't
146        # scroll the text too.
147        distance = self.player_sprite.right
148        left, bottom = self.cam.bottom_left
149        output = f"Distance: {distance:.0f}"
150        arcade.draw_text(
151            output, left + 10, bottom + 20, arcade.color.BLACK, 14
152        )
153
154        if self.game_over:
155            arcade.draw_text(
156                "Game Over",
157                left + 200,
158                bottom + 200,
159                arcade.color.BLACK,
160                30,
161            )
162
163    def on_key_press(self, key, modifiers):
164        """
165        Called whenever the mouse moves.
166        """
167        if key == arcade.key.UP:
168            if self.physics_engine.can_jump():
169                self.player_sprite.change_y = JUMP_SPEED
170        elif key == arcade.key.LEFT:
171            self.player_sprite.change_x = -MOVEMENT_SPEED
172        elif key == arcade.key.RIGHT:
173            self.player_sprite.change_x = MOVEMENT_SPEED
174
175    def on_key_release(self, key, modifiers):
176        """
177        Called when the user presses a mouse button.
178        """
179        if key == arcade.key.LEFT or key == arcade.key.RIGHT:
180            self.player_sprite.change_x = 0
181
182    def on_update(self, delta_time):
183        """Movement and game logic"""
184
185        if self.player_sprite.right >= self.end_of_map:
186            if self.level < self.max_level:
187                self.level += 1
188                self.load_level(self.level)
189                self.player_sprite.center_x = 128
190                self.player_sprite.center_y = 64
191                self.player_sprite.change_x = 0
192                self.player_sprite.change_y = 0
193            else:
194                self.game_over = True
195
196        # Call update on all sprites (The sprites don't do much in this
197        # example though.)
198        if not self.game_over:
199            self.physics_engine.update()
200
201            # --- Manage Scrolling ---
202
203            # Keep track of if we changed the boundary. We don't want to
204            # update the camera if we don't need to.
205            changed = False
206
207            pos = self.cam.position
208
209            top_left = self.cam.top_left
210
211            # Scroll left
212            left_boundary = top_left[0] + VIEWPORT_MARGIN_LEFT
213            if self.player_sprite.left < left_boundary:
214                changed = True
215                pos = pos[0] + (self.player_sprite.left - left_boundary), pos[1]
216
217            # Scroll up
218            top_boundary = top_left[1] - VIEWPORT_MARGIN_TOP
219            if self.player_sprite.top > top_boundary:
220                changed = True
221                pos = pos[0], pos[1] + (self.player_sprite.top - top_boundary)
222
223            bottom_right = self.cam.bottom_right
224
225            # Scroll right
226            right_boundary = bottom_right[0] - VIEWPORT_MARGIN_RIGHT
227            if self.player_sprite.right > right_boundary:
228                changed = True
229                pos = pos[0] + (self.player_sprite.right - right_boundary), pos[1]
230
231            # Scroll down
232            bottom_boundary = bottom_right[1] + VIEWPORT_MARGIN_BOTTOM
233            if self.player_sprite.bottom < bottom_boundary:
234                pos = pos[0], pos[1] + (self.player_sprite.bottom - bottom_boundary)
235
236            # If we changed the boundary values, update the view port to match
237            if changed:
238                self.cam.position = pos
239                # Make sure our boundaries are integer values. While the view port does
240                # support floating point numbers, for this application we want every pixel
241                # in the view port to map directly onto a pixel on the screen. We don't want
242                # any rounding errors.
243                bottom_left = self.cam.bottom_left
244                self.cam.bottom_left = int(bottom_left[0]), int(bottom_left[1])
245
246                self.cam.use()
247
248
249def main():
250    window = MyGame()
251    window.setup()
252    arcade.run()
253
254
255if __name__ == "__main__":
256    main()