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
 11import time
 12
 13import arcade
 14
 15TILE_SCALING = 0.5
 16PLAYER_SCALING = 0.5
 17
 18WINDOW_WIDTH = 1280
 19WINDOW_HEIGHT = 720
 20
 21WINDOW_TITLE = "Camera Example"
 22SPRITE_PIXEL_SIZE = 128
 23GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING
 24
 25# How many pixels to keep as a minimum margin between the character
 26# and the edge of the screen.
 27VIEWPORT_MARGIN_TOP = 60
 28VIEWPORT_MARGIN_BOTTOM = 60
 29VIEWPORT_RIGHT_MARGIN = 270
 30VIEWPORT_LEFT_MARGIN = 270
 31
 32# Physics
 33MOVEMENT_SPEED = 5
 34JUMP_SPEED = 23
 35GRAVITY = 1.1
 36
 37# Map Layers
 38LAYER_NAME_PLATFORMS = "Platforms"
 39LAYER_NAME_COINS = "Coins"
 40LAYER_NAME_BOMBS = "Bombs"
 41
 42
 43class GameView(arcade.View):
 44    """Main application class."""
 45
 46    def __init__(self):
 47        """
 48        Initializer
 49        """
 50        super().__init__()
 51
 52        # Our TileMap Object
 53        self.tile_map = None
 54
 55        # Our Scene Object
 56        self.scene = None
 57
 58        # Set up the player
 59        self.score = 0
 60        self.player_sprite = None
 61
 62        self.physics_engine = None
 63        self.top_of_map = 0
 64        self.end_of_map = 0
 65        self.game_over = False
 66        self.last_time = None
 67        self.frame_count = 0
 68        self.fps_message = None
 69
 70        # Cameras
 71        self.camera: arcade.camera.Camera2D = None
 72        self.gui_camera = None
 73
 74        self.camera_shake = 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            x=10,
 85            y=40,
 86            color=arcade.color.BLACK,
 87            font_size=14,
 88        )
 89        self.text_score = arcade.Text(
 90            f"Score: {self.score}",
 91            x=10,
 92            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        self.camera = arcade.camera.Camera2D()
135
136        self.camera_shake = arcade.camera.grips.ScreenShake2D(self.camera.view_data,
137                                                              max_amplitude=12.5,
138                                                              acceleration_duration=0.05,
139                                                              falloff_time=0.20,
140                                                              shake_frequency=15.0)
141
142        # Center camera on user
143        self.pan_camera_to_user()
144
145        # Calculate the right edge of the my_map in pixels
146        self.top_of_map = self.tile_map.height * GRID_PIXEL_SIZE
147        self.end_of_map = self.tile_map.width * GRID_PIXEL_SIZE
148
149        # --- Other stuff
150        # Set the background color
151        if self.tile_map.background_color:
152            self.background_color = self.tile_map.background_color
153
154        # Keep player from running through the wall_list layer
155        self.physics_engine = arcade.PhysicsEnginePlatformer(
156            self.player_sprite,
157            self.scene.get_sprite_list(LAYER_NAME_PLATFORMS),
158            gravity_constant=GRAVITY,
159        )
160
161        self.game_over = False
162
163    def on_resize(self, width, height):
164        """Resize window"""
165        super().on_resize(width, height)
166        self.camera.match_window()
167
168    def on_draw(self):
169        """Render the screen."""
170        self.clear()
171
172        self.camera_shake.update_camera()
173        with self.camera.activate():
174            # Draw our Scene
175            self.scene.draw()
176        # Readjust the camera so the screen shake doesn't affect
177        # the camera following algorithm.
178        self.camera_shake.readjust_camera()
179
180        with self.window.default_camera.activate():
181            # Update fps text periodically
182            if self.last_time and self.frame_count % 60 == 0:
183                fps = 1.0 / (time.time() - self.last_time) * 60
184                self.text_fps.text = f"FPS: {fps:5.2f}"
185
186            self.text_fps.draw()
187
188            if self.frame_count % 60 == 0:
189                self.last_time = time.time()
190
191            # Draw Score
192            self.text_score.draw()
193
194            # Draw game over
195            if self.game_over:
196                arcade.draw_text("Game Over", self.width/2, self.height/2, arcade.color.BLACK,
197                                 30)
198
199        self.frame_count += 1
200
201    def on_key_press(self, key, modifiers):
202        """
203        Called whenever a key is pressed
204        """
205        if key == arcade.key.UP:
206            if self.physics_engine.can_jump():
207                self.player_sprite.change_y = JUMP_SPEED
208        elif key == arcade.key.LEFT:
209            self.player_sprite.change_x = -MOVEMENT_SPEED
210        elif key == arcade.key.RIGHT:
211            self.player_sprite.change_x = MOVEMENT_SPEED
212
213    def on_key_release(self, key, modifiers):
214        """
215        Called when the user presses a mouse button.
216        """
217        if key == arcade.key.LEFT or key == arcade.key.RIGHT:
218            self.player_sprite.change_x = 0
219
220    def pan_camera_to_user(self, panning_fraction: float = 1.0):
221        """
222        Manage Scrolling
223
224        Args:
225            panning_fraction:
226                Number from 0 to 1. Higher the number, faster we
227                pan the camera to the user.
228        """
229
230        # This spot would center on the user
231        screen_center_x, screen_center_y = self.player_sprite.position
232        if screen_center_x < self.camera.viewport_width/2:
233            screen_center_x = self.camera.viewport_width/2
234        if screen_center_y < self.camera.viewport_height/2:
235            screen_center_y = self.camera.viewport_height/2
236        user_centered = screen_center_x, screen_center_y
237
238        self.camera.position = arcade.math.lerp_2d(
239            self.camera.position,
240            user_centered,
241            panning_fraction,
242        )
243
244    def on_update(self, delta_time):
245        """Movement and game logic"""
246
247        if self.player_sprite.right >= self.end_of_map:
248            self.game_over = True
249
250        # Call update on all sprites
251        if not self.game_over:
252            self.physics_engine.update()
253            self.camera_shake.update(delta_time)
254
255        coins_hit = arcade.check_for_collision_with_list(
256            self.player_sprite, self.scene.get_sprite_list("Coins")
257        )
258        for coin in coins_hit:
259            coin.remove_from_sprite_lists()
260            self.score += 1
261
262        # Bomb hits
263        bombs_hit = arcade.check_for_collision_with_list(
264            self.player_sprite, self.scene.get_sprite_list("Bombs")
265        )
266        for bomb in bombs_hit:
267            bomb.remove_from_sprite_lists()
268            self.camera_shake.start()
269
270        # Pan to the user
271        self.pan_camera_to_user(panning_fraction=0.12)
272
273        # Update score text
274        self.text_score.text = f"Score: {self.score}"
275
276
277def main():
278    """ Main function """
279    # Create a window class. This is what actually shows up on screen
280    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
281
282    # Create and setup the GameView
283    game = GameView()
284    game.setup()
285
286    # Show GameView on screen
287    window.show_view(game)
288
289    # Start the arcade game loop
290    arcade.run()
291
292
293if __name__ == "__main__":
294    main()