25. Sprites and Walls

25.1. Setup

Many games with sprites often have “walls” that the character can’t move through. There are rather straight-forward to create.

To begin with, let’s get a couple images. Our character, and a box that will act as a blocking wall. Both images are from kenney.nl.

../../_images/character1.png

images/character.png

../../_images/boxCrate_double.png

images/boxCrate_double.png

Start with a default file:

sprite_move_walls.py start
 1""" Sprite Sample Program """
 2
 3import arcade
 4
 5# --- Constants ---
 6SPRITE_SCALING_BOX = 0.5
 7SPRITE_SCALING_PLAYER = 0.5
 8
 9SCREEN_WIDTH = 800
10SCREEN_HEIGHT = 600
11
12MOVEMENT_SPEED = 5
13
14
15class MyGame(arcade.Window):
16    """ This class represents the main window of the game. """
17
18    def __init__(self):
19        """ Initializer """
20        # Call the parent class initializer
21        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprites With Walls Example")
22
23
24    def setup(self):
25        # Set the background color
26        arcade.set_background_color(arcade.color.AMAZON)
27
28    def on_draw(self):
29        arcade.start_render()
30
31
32def main():
33    window = MyGame()
34    window.setup()
35    arcade.run()
36
37
38if __name__ == "__main__":
39    main()

In the __init__ method, let’s create some variables to hold our sprites:

# Sprite lists
self.player_list = None
self.wall_list = None

# Set up the player
self.player_sprite = None

# This variable holds our simple "physics engine"
self.physics_engine = None

In the setup method, let’s create our sprite lists:

# Sprite lists
self.player_list = arcade.SpriteList()
self.wall_list = arcade.SpriteList()

Then reset the score and create the player:

# Reset the score
self.score = 0

# Create the player
self.player_sprite = arcade.Sprite("images/character.png", SPRITE_SCALING_PLAYER)
self.player_sprite.center_x = 50
self.player_sprite.center_y = 64
self.player_list.append(self.player_sprite)

Then go ahead and draw everything in our on_draw:

def on_draw(self):
    arcade.start_render()
    self.wall_list.draw()
    self.player_list.draw()

Run the program and make sure it works.

../../_images/just_player.png

25.2. Individually Placing Walls

In our setup method, we can position individual boxes to be used as “walls”:

# Manually create and position a box at 300, 200
wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
wall.center_x = 300
wall.center_y = 200
self.wall_list.append(wall)

# Manually create and position a box at 364, 200
wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
wall.center_x = 364
wall.center_y = 200
self.wall_list.append(wall)

Go ahead and try it out. It should look like:

../../_images/with_two_boxes.png

Full listing below:

sprite_move_walls.py Step 2
 1""" Sprite Sample Program """
 2
 3import arcade
 4
 5# --- Constants ---
 6SPRITE_SCALING_BOX = 0.5
 7SPRITE_SCALING_PLAYER = 0.5
 8
 9SCREEN_WIDTH = 800
10SCREEN_HEIGHT = 600
11
12MOVEMENT_SPEED = 5
13
14
15class MyGame(arcade.Window):
16    """ This class represents the main window of the game. """
17
18    def __init__(self):
19        """ Initializer """
20        # Call the parent class initializer
21        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")
22
23        # Sprite lists
24        self.player_list = None
25        self.wall_list = None
26
27        # Set up the player
28        self.player_sprite = None
29
30    def setup(self):
31
32        # Set the background color
33        arcade.set_background_color(arcade.color.AMAZON)
34
35        # Sprite lists
36        self.player_list = arcade.SpriteList()
37        self.wall_list = arcade.SpriteList()
38
39        # Reset the score
40        self.score = 0
41
42        # Create the player
43        self.player_sprite = arcade.Sprite("images/character.png", SPRITE_SCALING_PLAYER)
44        self.player_sprite.center_x = 50
45        self.player_sprite.center_y = 64
46        self.player_list.append(self.player_sprite)
47
48        # Manually create and position a box at 300, 200
49        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
50        wall.center_x = 300
51        wall.center_y = 200
52        self.wall_list.append(wall)
53
54        # Manually create and position a box at 364, 200
55        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
56        wall.center_x = 364
57        wall.center_y = 200
58        self.wall_list.append(wall)
59
60    def on_draw(self):
61        arcade.start_render()
62        self.wall_list.draw()
63        self.player_list.draw()
64
65
66def main():
67    """ Main method """
68    window = MyGame()
69    window.setup()
70    arcade.run()
71
72
73if __name__ == "__main__":
74    main()

25.3. Placing Walls With A Loop

In our setup method, we can create a row of box sprites using a for loop. In the code below, our y value is always 350, and we change the x value from 173 to 650. We put a box every 64 pixels because each box happens to be 64 pixels wide.

# Place boxes inside a loop
for x in range(173, 650, 64):
    wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
    wall.center_x = x
    wall.center_y = 350
    self.wall_list.append(wall)
../../_images/boxes_loop.png

25.4. Placing Walls With A List

You could even create a list of coordinates, and then just loop through that list creating walls:

# --- Place walls with a list
coordinate_list = [[400, 500],
                   [470, 500],
                   [400, 570],
                   [470, 570]]

# Loop through coordinates
for coordinate in coordinate_list:
    wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
    wall.center_x = coordinate[0]
    wall.center_y = coordinate[1]
    self.wall_list.append(wall)
../../_images/list.png

Full listing below:

sprite_move_walls.py Step 3
 1""" Sprite Sample Program """
 2
 3import arcade
 4
 5# --- Constants ---
 6SPRITE_SCALING_BOX = 0.5
 7SPRITE_SCALING_PLAYER = 0.5
 8
 9SCREEN_WIDTH = 800
10SCREEN_HEIGHT = 600
11
12MOVEMENT_SPEED = 5
13
14class MyGame(arcade.Window):
15    """ This class represents the main window of the game. """
16
17    def __init__(self):
18        """ Initializer """
19        # Call the parent class initializer
20        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")
21
22        # Sprite lists
23        self.player_list = None
24        self.wall_list = None
25
26        # Set up the player
27        self.player_sprite = None
28
29    def setup(self):
30
31        # Set the background color
32        arcade.set_background_color(arcade.color.AMAZON)
33
34        # Sprite lists
35        self.player_list = arcade.SpriteList()
36        self.wall_list = arcade.SpriteList()
37
38        # Reset the score
39        self.score = 0
40
41        # Create the player
42        self.player_sprite = arcade.Sprite("images/character.png", SPRITE_SCALING_PLAYER)
43        self.player_sprite.center_x = 50
44        self.player_sprite.center_y = 64
45        self.player_list.append(self.player_sprite)
46
47        # --- Manually place walls
48
49        # Manually create and position a box at 300, 200
50        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
51        wall.center_x = 300
52        wall.center_y = 200
53        self.wall_list.append(wall)
54
55        # Manually create and position a box at 364, 200
56        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
57        wall.center_x = 364
58        wall.center_y = 200
59        self.wall_list.append(wall)
60
61        # --- Place boxes inside a loop
62        for x in range(173, 650, 64):
63            wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
64            wall.center_x = x
65            wall.center_y = 350
66            self.wall_list.append(wall)
67
68        # --- Place walls with a list
69        coordinate_list = [[400, 500],
70                           [470, 500],
71                           [400, 570],
72                           [470, 570]]
73
74        # Loop through coordinates
75        for coordinate in coordinate_list:
76            wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
77            wall.center_x = coordinate[0]
78            wall.center_y = coordinate[1]
79            self.wall_list.append(wall)
80
81
82    def on_draw(self):
83        arcade.start_render()
84        self.player_list.draw()
85        self.wall_list.draw()
86
87
88def main():
89    """ Main method """
90    window = MyGame()
91    window.setup()
92    arcade.run()
93
94
95if __name__ == "__main__":
96    main()

25.5. Physics Engine

First, we need to hook the keyboard up to the player:

def on_key_press(self, key, modifiers):
    """Called whenever a key is pressed. """

    if key == arcade.key.UP:
        self.player_sprite.change_y = MOVEMENT_SPEED
    elif key == arcade.key.DOWN:
        self.player_sprite.change_y = -MOVEMENT_SPEED
    elif key == arcade.key.LEFT:
        self.player_sprite.change_x = -MOVEMENT_SPEED
    elif key == arcade.key.RIGHT:
        self.player_sprite.change_x = MOVEMENT_SPEED

def on_key_release(self, key, modifiers):
    """Called when the user releases a key. """

    if key == arcade.key.UP or key == arcade.key.DOWN:
        self.player_sprite.change_y = 0
    elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
        self.player_sprite.change_x = 0

Now, we need to add a way to stop the player from running into walls.

The Arcade Library has a built in “physics engine.” A physics engine handles the interactions between the virtual physical objects in the game. For example, a physics engine might be several balls running into each other, a character sliding down a hill, or a car making a turn on the road.

Physics engines have made impressive gains on what they can simulate. For our game, we’ll just keep things simple and make sure our character can’t walk through walls.

We’ll create variable to hold our physics engine in the __init__:

# This variable holds our simple "physics engine"
self.physics_engine = None

We can create the actual physics engine in our setup method with the following code:

self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list)

This identifies the player character (player_sprite), and a list of sprites (wall_list) that the player character isn’t allowed to pass through.

Before, we updated all the sprites with a self.all_sprites_list.update() command. With the physics engine, we will instead update the sprites by using the physics engine’s update:

def update(self, delta_time):
    self.physics_engine.update()

The simple physics engine follows the following algorithm:

  • Move the player in the x direction according to the player’s change_x value.

  • Check the player against the wall list and see if there are any collisions.

  • If the player is colliding:

    • If the player is moving right, set the player’s right edge to the wall’s left edge.

    • If the player is moving left, set the player’s left edge to the wall’s right edge.

    • If the player isn’t moving left or right, print out a message that we are confused how we hit something when we aren’t moving.

  • Then we just do the same thing, except with the y coordinates.

You can see the physics engine source code on GitHub.

Here’s the full example:

sprite_move_walls.py
  1""" Sprite Sample Program """
  2
  3import arcade
  4
  5# --- Constants ---
  6SPRITE_SCALING_BOX = 0.5
  7SPRITE_SCALING_PLAYER = 0.5
  8
  9SCREEN_WIDTH = 800
 10SCREEN_HEIGHT = 600
 11
 12MOVEMENT_SPEED = 5
 13
 14
 15class MyGame(arcade.Window):
 16    """ This class represents the main window of the game. """
 17
 18    def __init__(self):
 19        """ Initializer """
 20        # Call the parent class initializer
 21        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")
 22
 23        # Sprite lists
 24        self.player_list = None
 25        self.wall_list = None
 26
 27        # Set up the player
 28        self.player_sprite = None
 29
 30        # This variable holds our simple "physics engine"
 31        self.physics_engine = None
 32
 33    def setup(self):
 34
 35        # Set the background color
 36        arcade.set_background_color(arcade.color.AMAZON)
 37
 38        # Sprite lists
 39        self.player_list = arcade.SpriteList()
 40        self.wall_list = arcade.SpriteList()
 41
 42        # Reset the score
 43        self.score = 0
 44
 45        # Create the player
 46        self.player_sprite = arcade.Sprite("images/character.png", SPRITE_SCALING_PLAYER)
 47        self.player_sprite.center_x = 50
 48        self.player_sprite.center_y = 64
 49        self.player_list.append(self.player_sprite)
 50
 51        # --- Manually place walls
 52
 53        # Manually create and position a box at 300, 200
 54        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 55        wall.center_x = 300
 56        wall.center_y = 200
 57        self.wall_list.append(wall)
 58
 59        # Manually create and position a box at 364, 200
 60        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 61        wall.center_x = 364
 62        wall.center_y = 200
 63        self.wall_list.append(wall)
 64
 65        # --- Place boxes inside a loop
 66        for x in range(173, 650, 64):
 67            wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 68            wall.center_x = x
 69            wall.center_y = 350
 70            self.wall_list.append(wall)
 71
 72        # --- Place walls with a list
 73        coordinate_list = [[400, 500],
 74                           [470, 500],
 75                           [400, 570],
 76                           [470, 570]]
 77
 78        # Loop through coordinates
 79        for coordinate in coordinate_list:
 80            wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 81            wall.center_x = coordinate[0]
 82            wall.center_y = coordinate[1]
 83            self.wall_list.append(wall)
 84
 85        # Create the physics engine. Give it a reference to the player, and
 86        # the walls we can't run into.
 87        self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list)
 88
 89    def on_draw(self):
 90        arcade.start_render()
 91        self.wall_list.draw()
 92        self.player_list.draw()
 93
 94    def update(self, delta_time):
 95        self.physics_engine.update()
 96
 97    def on_key_press(self, key, modifiers):
 98        """Called whenever a key is pressed. """
 99
100        if key == arcade.key.UP:
101            self.player_sprite.change_y = MOVEMENT_SPEED
102        elif key == arcade.key.DOWN:
103            self.player_sprite.change_y = -MOVEMENT_SPEED
104        elif key == arcade.key.LEFT:
105            self.player_sprite.change_x = -MOVEMENT_SPEED
106        elif key == arcade.key.RIGHT:
107            self.player_sprite.change_x = MOVEMENT_SPEED
108
109    def on_key_release(self, key, modifiers):
110        """Called when the user releases a key. """
111
112        if key == arcade.key.UP or key == arcade.key.DOWN:
113            self.player_sprite.change_y = 0
114        elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
115            self.player_sprite.change_x = 0
116
117
118def main():
119    """ Main method """
120    window = MyGame()
121    window.setup()
122    arcade.run()
123
124
125if __name__ == "__main__":
126    main()

25.6. Using a View Port for Scrolling

What if one screen isn’t enough to hold your maze of walls? We can have a world that is larger than just our window. We do this by adjusting the view port. Normally coordinate (0, 0) is the lower left corner of our screen. We can change that! We could have an entire world stretch from (0, 0) to (3000, 3000), and have a smaller view port that was 800x640 that scrolled around that.

The command for using the view port is set_viewport. This command takes four arguments. The first two are the left and bottom boundaries of the window. By default these are zero. That is why (0, 0) is in the lower left of the screen. The next two commands are the top and right coordinates of the screen. By default these are the screen width and height, minus one. So an 800 pixel-wide window would have x-coordinates from 0 - 799.

A command like this would shift the whole “view” of the window 200 pixels to the right:

# Specify viewport size by (left, right, bottom, top)
arcade.set_viewport(200, 200 + SCREEN_WIDTH - 1, 0, SCREEN_HEIGHT - 1)

So with a 800 wide pixel window, we would show x-coordinates 200 - 999 instead of 0 - 799.

Instead of hard-coding the shift at 200 pixels, we need to use a variable and have rules around when to shift the view. In our next example, we will create two new instance variables in our application class that hold the left and bottom coordinates for our view port. We’ll default to zero.

self.view_left = 0
self.view_bottom = 0

We are also going to create two new constants. We don’t want the player to reach the edge of the screen before we start scrolling. Because then the player would have no idea where she is going. In our example we will set a “margin” of 150 pixels. When the player is 150 pixels from the edge of the screen, we’ll move the view port so she can see at least 150 pixels around her.

VIEWPORT_MARGIN = 150

Next, in our update method, we need to see if the user has moved too close to the edge of the screen and we need to update the boundaries.

# Keep track of if we changed the boundary. We don't want to call the
# set_viewport command if we didn't change the view port.
changed = False

# Scroll left
left_boundary = self.view_left + VIEWPORT_MARGIN
if self.player_sprite.left < left_boundary:
    self.view_left -= left_boundary - self.player_sprite.left
    changed = True

# Scroll right
right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN
if self.player_sprite.right > right_boundary:
    self.view_left += self.player_sprite.right - right_boundary
    changed = True

# Scroll up
top_boundary = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN
if self.player_sprite.top > top_boundary:
    self.view_bottom += self.player_sprite.top - top_boundary
    changed = True

# Scroll down
bottom_boundary = self.view_bottom + VIEWPORT_MARGIN
if self.player_sprite.bottom < bottom_boundary:
    self.view_bottom -= bottom_boundary - self.player_sprite.bottom
    changed = True

# Make sure our boundaries are integer values. While the view port does
# support floating point numbers, for this application we want every pixel
# in the view port to map directly onto a pixel on the screen. We don't want
# any rounding errors.
self.view_left = int(self.view_left)
self.view_bottom = int(self.view_bottom)

# If we changed the boundary values, update the view port to match
if changed:
    arcade.set_viewport(self.view_left,
                        SCREEN_WIDTH + self.view_left - 1,
                        self.view_bottom,
                        SCREEN_HEIGHT + self.view_bottom - 1)

The full example is below:

sprite_move_scrolling.py
  1""" Sprite Sample Program """
  2
  3import arcade
  4
  5# --- Constants ---
  6SPRITE_SCALING_BOX = 0.5
  7SPRITE_SCALING_PLAYER = 0.5
  8
  9SCREEN_WIDTH = 800
 10SCREEN_HEIGHT = 600
 11
 12MOVEMENT_SPEED = 5
 13
 14VIEWPORT_MARGIN = 150
 15
 16
 17class MyGame(arcade.Window):
 18    """ This class represents the main window of the game. """
 19
 20    def __init__(self):
 21        """ Initializer """
 22        # Call the parent class initializer
 23        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")
 24
 25        # Sprite lists
 26        self.player_list = None
 27        self.wall_list = None
 28
 29        # Set up the player
 30        self.player_sprite = None
 31
 32        # This variable holds our simple "physics engine"
 33        self.physics_engine = None
 34
 35        # Manage the view port
 36        self.view_left = 0
 37        self.view_bottom = 0
 38
 39    def setup(self):
 40
 41        # Set the background color
 42        arcade.set_background_color(arcade.color.AMAZON)
 43
 44        # Reset the view port
 45        self.view_left = 0
 46        self.view_bottom = 0
 47
 48        # Sprite lists
 49        self.player_list = arcade.SpriteList()
 50        self.wall_list = arcade.SpriteList()
 51
 52        # Reset the score
 53        self.score = 0
 54
 55        # Create the player
 56        self.player_sprite = arcade.Sprite("images/character.png", SPRITE_SCALING_PLAYER)
 57        self.player_sprite.center_x = 50
 58        self.player_sprite.center_y = 64
 59        self.player_list.append(self.player_sprite)
 60
 61        # --- Manually place walls
 62
 63        # Manually create and position a box at 300, 200
 64        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 65        wall.center_x = 300
 66        wall.center_y = 200
 67        self.wall_list.append(wall)
 68
 69        # Manually create and position a box at 364, 200
 70        wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 71        wall.center_x = 364
 72        wall.center_y = 200
 73        self.wall_list.append(wall)
 74
 75        # --- Place boxes inside a loop
 76        for x in range(173, 650, 64):
 77            wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 78            wall.center_x = x
 79            wall.center_y = 350
 80            self.wall_list.append(wall)
 81
 82        # --- Place walls with a list
 83        coordinate_list = [[400, 500],
 84                           [470, 500],
 85                           [400, 570],
 86                           [470, 570]]
 87
 88        # Loop through coordinates
 89        for coordinate in coordinate_list:
 90            wall = arcade.Sprite("images/boxCrate_double.png", SPRITE_SCALING_BOX)
 91            wall.center_x = coordinate[0]
 92            wall.center_y = coordinate[1]
 93            self.wall_list.append(wall)
 94
 95        # Create the physics engine. Give it a reference to the player, and
 96        # the walls we can't run into.
 97        self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list)
 98
 99    def on_draw(self):
100        arcade.start_render()
101        self.wall_list.draw()
102        self.player_list.draw()
103
104    def update(self, delta_time):
105        self.physics_engine.update()
106
107        # --- Manage Scrolling ---
108
109        # Keep track of if we changed the boundary. We don't want to call the
110        # set_viewport command if we didn't change the view port.
111        changed = False
112
113        # Scroll left
114        left_boundary = self.view_left + VIEWPORT_MARGIN
115        if self.player_sprite.left < left_boundary:
116            self.view_left -= left_boundary - self.player_sprite.left
117            changed = True
118
119        # Scroll right
120        right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN
121        if self.player_sprite.right > right_boundary:
122            self.view_left += self.player_sprite.right - right_boundary
123            changed = True
124
125        # Scroll up
126        top_boundary = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN
127        if self.player_sprite.top > top_boundary:
128            self.view_bottom += self.player_sprite.top - top_boundary
129            changed = True
130
131        # Scroll down
132        bottom_boundary = self.view_bottom + VIEWPORT_MARGIN
133        if self.player_sprite.bottom < bottom_boundary:
134            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
135            changed = True
136
137        # Make sure our boundaries are integer values. While the view port does
138        # support floating point numbers, for this application we want every pixel
139        # in the view port to map directly onto a pixel on the screen. We don't want
140        # any rounding errors.
141        self.view_left = int(self.view_left)
142        self.view_bottom = int(self.view_bottom)
143
144        # If we changed the boundary values, update the view port to match
145        if changed:
146            arcade.set_viewport(self.view_left,
147                                SCREEN_WIDTH + self.view_left - 1,
148                                self.view_bottom,
149                                SCREEN_HEIGHT + self.view_bottom - 1)
150
151    def on_key_press(self, key, modifiers):
152        """ Called whenever a key is pressed. """
153
154        if key == arcade.key.UP:
155            self.player_sprite.change_y = MOVEMENT_SPEED
156        elif key == arcade.key.DOWN:
157            self.player_sprite.change_y = -MOVEMENT_SPEED
158        elif key == arcade.key.LEFT:
159            self.player_sprite.change_x = -MOVEMENT_SPEED
160        elif key == arcade.key.RIGHT:
161            self.player_sprite.change_x = MOVEMENT_SPEED
162
163    def on_key_release(self, key, modifiers):
164        """Called when the user releases a key. """
165
166        if key == arcade.key.UP or key == arcade.key.DOWN:
167            self.player_sprite.change_y = 0
168        elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
169            self.player_sprite.change_x = 0
170
171
172def main():
173    """ Main method """
174    window = MyGame()
175    window.setup()
176    arcade.run()
177
178
179if __name__ == "__main__":
180    main()