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