Camera Use in a Platformer#

Screen shot of using a scrolling window
camera_platform.py#
  1"""
  2Camera Example
  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.camera_platform
  9"""
 10
 11from __future__ import annotations
 12
 13import time
 14
 15import arcade
 16
 17TILE_SCALING = 0.5
 18PLAYER_SCALING = 0.5
 19
 20SCREEN_WIDTH = 800
 21SCREEN_HEIGHT = 600
 22
 23SCREEN_TITLE = "Camera Example"
 24SPRITE_PIXEL_SIZE = 128
 25GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING
 26
 27# How many pixels to keep as a minimum margin between the character
 28# and the edge of the screen.
 29VIEWPORT_MARGIN_TOP = 60
 30VIEWPORT_MARGIN_BOTTOM = 60
 31VIEWPORT_RIGHT_MARGIN = 270
 32VIEWPORT_LEFT_MARGIN = 270
 33
 34# Physics
 35MOVEMENT_SPEED = 5
 36JUMP_SPEED = 23
 37GRAVITY = 1.1
 38
 39# Map Layers
 40LAYER_NAME_PLATFORMS = "Platforms"
 41LAYER_NAME_COINS = "Coins"
 42LAYER_NAME_BOMBS = "Bombs"
 43
 44
 45class MyGame(arcade.Window):
 46    """Main application class."""
 47
 48    def __init__(self):
 49        """
 50        Initializer
 51        """
 52        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE, resizable=True)
 53
 54        # Our TileMap Object
 55        self.tile_map = None
 56
 57        # Our Scene Object
 58        self.scene = None
 59
 60        # Set up the player
 61        self.score = 0
 62        self.player_sprite = None
 63
 64        self.physics_engine = None
 65        self.top_of_map = 0
 66        self.end_of_map = 0
 67        self.game_over = False
 68        self.last_time = None
 69        self.frame_count = 0
 70        self.fps_message = None
 71
 72        # Cameras
 73        self.camera = None
 74        self.gui_camera = None
 75
 76        self.shake_offset_1 = 0
 77        self.shake_offset_2 = 0
 78        self.shake_vel_1 = 0
 79        self.shake_vel_2 = 0
 80
 81        # Text
 82        self.text_fps = arcade.Text(
 83            "",
 84            start_x=10,
 85            start_y=40,
 86            color=arcade.color.BLACK,
 87            font_size=14,
 88        )
 89        self.text_score = arcade.Text(
 90            f"Score: {self.score}",
 91            start_x=10,
 92            start_y=20,
 93            color=arcade.color.BLACK,
 94            font_size=14,
 95        )
 96
 97    def setup(self):
 98        """Set up the game and initialize the variables."""
 99
100        # Map name
101        map_name = ":resources:tiled_maps/level_1.json"
102
103        # Layer Specific Options for the Tilemap
104        layer_options = {
105            LAYER_NAME_PLATFORMS: {
106                "use_spatial_hash": True,
107            },
108            LAYER_NAME_COINS: {
109                "use_spatial_hash": True,
110            },
111            LAYER_NAME_BOMBS: {
112                "use_spatial_hash": True,
113            },
114        }
115
116        # Load in TileMap
117        self.tile_map = arcade.load_tilemap(map_name, TILE_SCALING, layer_options)
118
119        # Initiate New Scene with our TileMap, this will automatically add all layers
120        # from the map as SpriteLists in the scene in the proper order.
121        self.scene = arcade.Scene.from_tilemap(self.tile_map)
122
123        # Set up the player
124        self.player_sprite = arcade.Sprite(
125            ":resources:images/animated_characters/female_person/femalePerson_idle.png",
126            scale=PLAYER_SCALING,
127        )
128
129        # Starting position of the player
130        self.player_sprite.center_x = 196
131        self.player_sprite.center_y = 128
132        self.scene.add_sprite("Player", self.player_sprite)
133
134        viewport = (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
135        self.camera = arcade.Camera(viewport=viewport)
136        self.gui_camera = arcade.Camera(viewport=viewport)
137
138        # Center camera on user
139        self.pan_camera_to_user()
140
141        # Calculate the right edge of the my_map in pixels
142        self.top_of_map = self.tile_map.height * GRID_PIXEL_SIZE
143        self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
144
145        # --- Other stuff
146        # Set the background color
147        if self.tile_map.background_color:
148            self.background_color = self.tile_map.background_color
149
150        # Keep player from running through the wall_list layer
151        self.physics_engine = arcade.PhysicsEnginePlatformer(
152            self.player_sprite,
153            self.scene.get_sprite_list(LAYER_NAME_PLATFORMS),
154            gravity_constant=GRAVITY,
155        )
156
157        self.game_over = False
158
159    def on_resize(self, width, height):
160        """Resize window"""
161        self.camera.resize(width, height)
162        self.gui_camera.resize(width, height)
163
164    def on_draw(self):
165        """Render the screen."""
166        self.clear()
167
168        self.camera.use()
169
170        # Draw our Scene
171        self.scene.draw()
172
173        self.gui_camera.use()
174
175        # Update fps text periodically
176        if self.last_time and self.frame_count % 60 == 0:
177            fps = 1.0 / (time.time() - self.last_time) * 60
178            self.text_fps.text = f"FPS: {fps:5.2f}"
179
180        self.text_fps.draw()
181
182        if self.frame_count % 60 == 0:
183            self.last_time = time.time()
184
185        # Draw Score
186        self.text_score.draw()
187
188        # Draw game over
189        if self.game_over:
190            x = 200 + self.camera.position[0]
191            y = 200 + self.camera.position[1]
192            arcade.draw_text("Game Over", x, y, arcade.color.BLACK, 30)
193
194        self.frame_count += 1
195
196    def on_key_press(self, key, modifiers):
197        """
198        Called whenever a key is pressed
199        """
200        if key == arcade.key.UP:
201            if self.physics_engine.can_jump():
202                self.player_sprite.change_y = JUMP_SPEED
203        elif key == arcade.key.LEFT:
204            self.player_sprite.change_x = -MOVEMENT_SPEED
205        elif key == arcade.key.RIGHT:
206            self.player_sprite.change_x = MOVEMENT_SPEED
207
208    def on_key_release(self, key, modifiers):
209        """
210        Called when the user presses a mouse button.
211        """
212        if key == arcade.key.LEFT or key == arcade.key.RIGHT:
213            self.player_sprite.change_x = 0
214
215    def pan_camera_to_user(self, panning_fraction: float = 1.0):
216        """
217        Manage Scrolling
218
219        :param panning_fraction: Number from 0 to 1. Higher the number, faster we
220                                 pan the camera to the user.
221        """
222
223        # This spot would center on the user
224        screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
225        screen_center_y = self.player_sprite.center_y - (
226            self.camera.viewport_height / 2
227        )
228        if screen_center_x < 0:
229            screen_center_x = 0
230        if screen_center_y < 0:
231            screen_center_y = 0
232        user_centered = screen_center_x, screen_center_y
233
234        self.camera.move_to(user_centered, panning_fraction)
235
236    def on_update(self, delta_time):
237        """Movement and game logic"""
238
239        if self.player_sprite.right >= self.end_of_map:
240            self.game_over = True
241
242        # Call update on all sprites
243        if not self.game_over:
244            self.physics_engine.update()
245
246        coins_hit = arcade.check_for_collision_with_list(
247            self.player_sprite, self.scene.get_sprite_list("Coins")
248        )
249        for coin in coins_hit:
250            coin.remove_from_sprite_lists()
251            self.score += 1
252
253        # Bomb hits
254        bombs_hit = arcade.check_for_collision_with_list(
255            self.player_sprite, self.scene.get_sprite_list("Bombs")
256        )
257        for bomb in bombs_hit:
258            bomb.remove_from_sprite_lists()
259            print("Pow")
260            self.camera.shake((4, 7))
261
262        # Pan to the user
263        self.pan_camera_to_user(panning_fraction=0.12)
264
265        # Update score text
266        self.text_score.text = f"Score: {self.score}"
267
268
269def main():
270    """Get this game started."""
271    window = MyGame()
272    window.setup()
273    arcade.run()
274
275
276if __name__ == "__main__":
277    main()