Move with a Scrolling Screen - Margins

Unlike the Move with a Scrolling Screen - Centered which centers the camera on the player, this example only moves the camera if the user gets within so many pixels of the edge. It allows for a ‘box’ in the middle where the user can move around and NOT move the camera.

Screen shot of using a scrolling window
sprite_move_scrolling_box.py
  1"""
  2Scroll around a large screen.
  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.sprite_move_scrolling_box
  8"""
  9
 10import random
 11import arcade
 12
 13SPRITE_SCALING = 0.5
 14
 15WINDOW_WIDTH = 1280
 16WINDOW_HEIGHT = 720
 17WINDOW_TITLE = "Sprite Move with Scrolling Screen Example"
 18
 19# How many pixels to keep as a minimum margin between the character
 20# and the edge of the screen.
 21VIEWPORT_MARGIN = 200
 22HORIZONTAL_BOUNDARY = WINDOW_WIDTH / 2.0 - VIEWPORT_MARGIN
 23VERTICAL_BOUNDARY = WINDOW_HEIGHT / 2.0 - VIEWPORT_MARGIN
 24# If the player moves further than this boundary away from the camera we use a
 25# constraint to move the camera
 26CAMERA_BOUNDARY = arcade.LRBT(
 27    -HORIZONTAL_BOUNDARY,
 28      HORIZONTAL_BOUNDARY,
 29      -VERTICAL_BOUNDARY,
 30      VERTICAL_BOUNDARY,
 31)
 32
 33# How fast the camera pans to the player. 1.0 is instant.
 34CAMERA_SPEED = 0.1
 35
 36# How fast the character moves
 37PLAYER_MOVEMENT_SPEED = 7
 38
 39
 40class GameView(arcade.View):
 41    """ Main application class. """
 42
 43    def __init__(self):
 44        """
 45        Initializer
 46        """
 47        super().__init__()
 48
 49        # Sprite lists
 50        self.player_list = None
 51        self.wall_list = None
 52
 53        # Set up the player
 54        self.player_sprite = None
 55
 56        self.physics_engine = None
 57
 58        # Track the current state of what key is pressed
 59        self.left_pressed = False
 60        self.right_pressed = False
 61        self.up_pressed = False
 62        self.down_pressed = False
 63
 64        self.camera_sprites = arcade.camera.Camera2D()
 65        self.camera_gui = arcade.camera.Camera2D()
 66
 67    def setup(self):
 68        """ Set up the game and initialize the variables. """
 69
 70        # Sprite lists
 71        self.player_list = arcade.SpriteList()
 72        self.wall_list = arcade.SpriteList()
 73
 74        # Set up the player
 75        self.player_sprite = arcade.Sprite(
 76            ":resources:images/animated_characters/female_person/femalePerson_idle.png",
 77            scale=0.4,
 78        )
 79        self.player_sprite.center_x = 256
 80        self.player_sprite.center_y = 512
 81        self.player_list.append(self.player_sprite)
 82
 83        # -- Set up several columns of walls
 84        for x in range(200, 1650, 210):
 85            for y in range(0, 1600, 64):
 86                # Randomly skip a box so the player can find a way through
 87                if random.randrange(5) > 0:
 88                    wall = arcade.Sprite(
 89                        ":resources:images/tiles/grassCenter.png",
 90                        scale=SPRITE_SCALING,
 91                    )
 92                    wall.center_x = x
 93                    wall.center_y = y
 94                    self.wall_list.append(wall)
 95
 96        self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list)
 97
 98        # Set the background color
 99        self.background_color = arcade.color.AMAZON
100
101    def on_draw(self):
102        """
103        Render the screen.
104        """
105
106        # This command has to happen before we start drawing
107        self.clear()
108
109        # Select the camera we'll use to draw all our sprites
110        self.camera_sprites.use()
111
112        # Draw all the sprites.
113        self.wall_list.draw()
114        self.player_list.draw()
115
116        # Draw the box that we work to make sure the user stays inside of.
117        # This is just for illustration purposes. You'd want to remove this
118        # in your game.
119        camera_x, camera_y = self.camera_sprites.position
120        arcade.draw_rect_outline(
121            arcade.XYWH(camera_x, camera_y, CAMERA_BOUNDARY.width, CAMERA_BOUNDARY.height),
122            arcade.color.RED, 2
123        )
124
125        # Select the (unscrolled) camera for our GUI
126        self.camera_gui.use()
127
128        # Draw the GUI
129        arcade.draw_rect_filled(
130            arcade.rect.XYWH(self.width // 2, 20, self.width, 40),
131            color=arcade.color.ALMOND
132        )
133        text = (
134            f"Scroll value: ("
135            f"{self.camera_sprites.position[0]:5.1f}",
136            f"{self.camera_sprites.position[1]:5.1f})"
137        )
138        arcade.draw_text(text, 10, 10, arcade.color.BLACK_BEAN, 20)
139
140
141    def on_key_press(self, key, modifiers):
142        """Called whenever a key is pressed. """
143
144        if key == arcade.key.UP:
145            self.up_pressed = True
146        elif key == arcade.key.DOWN:
147            self.down_pressed = True
148        elif key == arcade.key.LEFT:
149            self.left_pressed = True
150        elif key == arcade.key.RIGHT:
151            self.right_pressed = True
152
153    def on_key_release(self, key, modifiers):
154        """Called when the user releases a key. """
155
156        if key == arcade.key.UP:
157            self.up_pressed = False
158        elif key == arcade.key.DOWN:
159            self.down_pressed = False
160        elif key == arcade.key.LEFT:
161            self.left_pressed = False
162        elif key == arcade.key.RIGHT:
163            self.right_pressed = False
164
165    def on_update(self, delta_time):
166        """ Movement and game logic """
167
168        # Calculate speed based on the keys pressed
169        self.player_sprite.change_x = 0
170        self.player_sprite.change_y = 0
171
172        if self.up_pressed and not self.down_pressed:
173            self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
174        elif self.down_pressed and not self.up_pressed:
175            self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
176        if self.left_pressed and not self.right_pressed:
177            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
178        elif self.right_pressed and not self.left_pressed:
179            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
180
181        # Call update on all sprites (The sprites don't do much in this
182        # example though.)
183        self.physics_engine.update()
184
185        # Scroll the screen to the player
186        self.scroll_to_player()
187
188    def scroll_to_player(self):
189        """
190        Scroll the window to the player.
191        This method will attempt to keep the player at least VIEWPORT_MARGIN
192        pixels away from the edge.
193
194        if CAMERA_SPEED is 1, the camera will immediately move to the desired position.
195        Anything between 0 and 1 will have the camera move to the location with a smoother
196        pan.
197        """
198
199        # --- Manage Scrolling ---
200        new_position = arcade.camera.grips.constrain_boundary_xy(
201            self.camera_sprites.view_data, CAMERA_BOUNDARY, self.player_sprite.position
202        )
203
204        self.camera_sprites.position = arcade.math.lerp_2d(
205            self.camera_sprites.position, (new_position[0], new_position[1]), CAMERA_SPEED
206        )
207
208    def on_resize(self, width: int, height: int):
209        """
210        Resize window
211        Handle the user grabbing the edge and resizing the window.
212        """
213        super().on_resize(width, height)
214        self.camera_sprites.match_window()
215        self.camera_gui.match_window(position=True)
216
217
218def main():
219    """ Main function """
220    # Create a window class. This is what actually shows up on screen
221    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
222
223    # Create and setup the GameView
224    game = GameView()
225    game.setup()
226
227    # Show GameView on screen
228    window.show_view(game)
229
230    # Start the arcade game loop
231    arcade.run()
232
233
234if __name__ == "__main__":
235    main()