Step 3 - Many Sprites with SpriteList#

So far our game is coming along nicely, we have a character on the screen! Wouldn’t it be nice if our character had a world to live in? In order to do that we’ll need to draw a lot more sprites. In this chapter we will explore SpriteList, a class Arcade provides to draw tons of Sprites at once.

At the end, we’ll have something like this:

../../_images/title_03.png

SpriteList#

arcade.SpriteList exists to draw a collection of Sprites all at once. Let’s say for example that you have 100,000 box Sprites that you want to draw. Without SpriteList you would have to put all of your sprites into a list, and then run a for loop over that which calls draw() on every sprite.

This approach is extremely un-performant. Instead, you can add all of your boxes to a arcade.SpriteList and then draw the SpriteList. Doing this, you are able to draw all 100,000 sprites for approximately the exact same cost as drawing one sprite.

Note

This is due to Arcade being a heavily GPU based library. GPUs are really good at doing things in batches. This means we can send all the information about our sprites to the GPU, and then tell it to draw them all at once. However if we just draw one sprite at a time, then we have to go on a round trip from our CPU to our GPU every time.

Even if you are only drawing one Sprite, you should still create a SpriteList for it. Outside of small debugging it is never better to draw an individual Sprite than it is to add it to a SpriteList. In fact, calling draw() on a Sprite just creates a SpriteList internally to draw that Sprite with.

Let’s go ahead and create one for our player inside our __init__ function, and add the player to it.

self.player_list = arcade.SpriteList()
self.player_list.append(self.player_sprite)

Then in our on_draw function, we can draw the SpriteList for the character instead of drawing the Sprite directly:

self.player_list.draw()

Now let’s try and build a world for our character. To do this, we’ll create a new SpriteList for the objects we’ll draw, we can do this in our __init__ function.

self.wall_list = arcade.SpriteList(use_spatial_hash=True)

There’s a little bit to unpack in this snippet of code. Let’s address each issue:

  1. Why not just use the same SpriteList we used for our player, and why is it named walls?

    Eventually we will want to do collision detection between our character and these objects. In addition to drawing, SpriteLists also serve as a utility for collision detection. You can for example check for collisions between two SpriteLists, or pass SpriteLists into several physics engines. We will explore these topics in later chapters.

  2. What is use_spatial_hash?

    This is also for collision detection. Spatial Hashing is a special algorithm which will make it much more performant, at the cost of being more expensive to move sprites. You will often see this option enabled on SpriteLists which are not expected to move much, such as walls or a floor.

With our newly created SpriteList, let’s go ahead and add some objects to it. We can add these lines to our __init__ function.

for x in range(0, 1250, 64):
    wall = arcade.Sprite(":resources:images/tiles/grassMid.png", TILE_SCALING)
    wall.center_x = x
    wall.center_y = 32
    self.wall_list.append(wall)

coordinate_list = [[512, 96], [256, 96], [768, 96]]
for coordinate in coordinate_list:
    wall = arcade.Sprite(
        ":resources:images/tiles/boxCrate_double.png", scale=0.5
    )
    wall.position = coordinate
    self.wall_list.append(wall)

In these lines, we’re adding some grass and some crates to our SpriteList.

For the ground we’re using Python’s range function to iterate on a list of X positions, which will give us a horizontal line of Sprites. For the boxes, we’re inserting them at specified coordinates from a list.

We’re also doing a few new things in the arcade.Sprite creation. First off we are passing the image file directly instead of creating a texture first. This is ultimately doing the same thing, we’re just not managing the texture ourselves, and letting Arcade handle it. We are also adding a scale to these sprites. For fun you can remove the scale, and see how the images will be much larger.

Finally all we need to do in order to draw our new world, is draw the SpriteList for walls in on_draw:

self.wall_list.draw()

Source Code#

03_more_sprites - Many Sprites with a SpriteList#
 1"""
 2Platformer Game
 3
 4python -m arcade.examples.platform_tutorial.03_more_sprites
 5"""
 6import arcade
 7
 8# Constants
 9SCREEN_WIDTH = 800
10SCREEN_HEIGHT = 600
11SCREEN_TITLE = "Platformer"
12
13# Constants used to scale our sprites from their original size
14TILE_SCALING = 0.5
15
16
17class MyGame(arcade.Window):
18    """
19    Main application class.
20    """
21
22    def __init__(self):
23
24        # Call the parent class and set up the window
25        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
26
27        # Variable to hold our texture for our player
28        self.player_texture = arcade.load_texture(":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png")
29
30        # Separate variable that holds the player sprite
31        self.player_sprite = arcade.Sprite(self.player_texture)
32        self.player_sprite.center_x = 64
33        self.player_sprite.center_y = 128
34
35        # SpriteList for our player
36        self.player_list = arcade.SpriteList()
37        self.player_list.append(self.player_sprite)
38
39        # SpriteList for our boxes and ground
40        # Putting our ground and box Sprites in the same SpriteList
41        # will make it easier to perform collision detection against
42        # them later on. Setting the spatial hash to True will make
43        # collision detection much faster if the objects in this
44        # SpriteList do not move.
45        self.wall_list = arcade.SpriteList(use_spatial_hash=True)
46
47        # Create the ground
48        # This shows using a loop to place multiple sprites horizontally
49        for x in range(0, 1250, 64):
50            wall = arcade.Sprite(":resources:images/tiles/grassMid.png", scale=0.5)
51            wall.center_x = x
52            wall.center_y = 32
53            self.wall_list.append(wall)
54
55        # Put some crates on the ground
56        # This shows using a coordinate list to place sprites
57        coordinate_list = [[512, 96], [256, 96], [768, 96]]
58
59        for coordinate in coordinate_list:
60            # Add a crate on the ground
61            wall = arcade.Sprite(
62                ":resources:images/tiles/boxCrate_double.png", scale=0.5
63            )
64            wall.position = coordinate
65            self.wall_list.append(wall)
66
67        self.background_color = arcade.csscolor.CORNFLOWER_BLUE
68
69    def setup(self):
70        """Set up the game here. Call this function to restart the game."""
71        pass
72
73    def on_draw(self):
74        """Render the screen."""
75
76        # Clear the screen to the background color
77        self.clear()
78
79        # Draw our sprites
80        self.player_list.draw()
81        self.wall_list.draw()
82
83
84def main():
85    """Main function"""
86    window = MyGame()
87    window.setup()
88    arcade.run()
89
90
91if __name__ == "__main__":
92    main()

Note

Once you have the code up and working, try-out the following:

  • See if you can change the colors of all the boxes and ground using the SpriteList

  • Try and make a SpriteList invisible

Run This Chapter#

python -m arcade.examples.platform_tutorial.03_more_sprites