21. Sprites And Collisions

Our games need support for handling objects that collide. Balls bouncing off paddles, laser beams hitting aliens, or our favorite character collecting a coin. All these examples require collision detection.

The Arcade library has support for sprites. A sprite is a two-dimensional image that is part of the larger graphical scene. Typically a sprite will be some kind of object in the scene that will be interacted with like a car, frog, or little plumber guy.

../../_images/sprite.png

Originally, video game consoles had built-in hardware support for sprites. Now this specialized hardware support is no longer needed, but we still use the term “sprite.” The history of sprites is interesting, if you want to read up more about it.

21.1. Finding Images for Sprites

There are several image formats that computers use:

  • .bmp - Bitmap. This is an uncompressed image that normally uses three bytes to represent each dot in the image. These files can be very large. Because there are so many better options, this format is not used often. It is, however, the simplest format to use.

  • .png - Great patent-free format for line art and clip art. Not great for photos. Can’t hold animations.

  • .gif - Great format for line art and clip art. Has had issues with patents (now expired). Can do animations.

  • .jpg - Great file format for photos. Terrible for clip-art. Don’t use for sprites.

  • .svg - File format for storing line-art images that can scale to any resolution. Not compatible with the “arcade” library.

If you use Google’s advanced image search you can find images that are “icon” sized, and either png or gif file format.

There’s also a great source for images from kenney.nl. He has a lot of free and cheap game image assets. That’s where the following images come from that we will use in our examples:

../../_images/character.png

character.png

../../_images/coin_01.png

coin_01.png

There are also some sprites that come with the Arcade library. A list of those sprites is available at:

https://api.arcade.academy/en/latest/resources.html

You can load these using a filename pattern such as: :resources:images/topdown_tanks/tank_red.png

21.1.1. Where to Save Images

Where should you save them? If you load your sprite with the code below, the computer will look for the character.png image in the same directory as your Python file. Save the image anywhere else, and it won’t be found.

21.1.2. How to Reference Images

If you create your own game that you publish, you need to:

  • Create your own images

  • Hire someone to create your images

  • Buy your own images with a license to use them in your own game

  • Find images that are public domain or licensed for public use

If you are just creating a game for class that won’t be used in public, then right before you load the image leave a comment with the source. I’ll show this in a bit.

Attention

Do not list “Google” as a source. That’s like using “The Library” as a source in your report. Find the source of the image that Google is pointing to.

21.2. Basic Sprites and Collisions

Let’s step through an example program that uses sprites. This example shows how to create a screen of sprites that are coins, and collect them using a sprite that is a character image controlled by the mouse as shown in the figure below. The program keeps “score” on how many coins have been collected. The code for this example may be found at:

https://api.arcade.academy/en/latest/examples/sprite_collect_coins.html

In this chapter, we’ll step through that example.

../../_images/collect_coins.gif

Example Sprite Game

21.2.1. Getting the Application Started

The first few lines of our program start off like other games we’ve done. We import a couple libraries. Set a couple constants for the size of the screen, and a couple new constants that we will use to scale our graphics.

The example below should have nothing new, it just creates a window and sets a background color. We’ll add in the new stuff soon.

Sprite Sample Start
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
""" Sprite Sample Program """

import random
import arcade

# --- Constants ---
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_COIN = 0.2
COIN_COUNT = 50

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class MyGame(arcade.Window):
    """ Our custom Window Class"""

    def __init__(self):
        """ Initializer """
        # Call the parent class initializer
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")

    def on_draw(self):
        arcade.start_render()


def main():
    """ Main method """
    window = MyGame()
    arcade.run()


if __name__ == "__main__":
    main()

21.2.2. The Constructor

What’s next? We need to add our attributes to the MyGame class. We add our attributes to the __init__ method. Here is our code with the expanded __init__:

Expanded Init
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
""" Sprite Sample Program """

import random
import arcade

# --- Constants ---
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_COIN = 0.2
COIN_COUNT = 50

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class MyGame(arcade.Window):
    """ Our custom Window Class"""

    def __init__(self):
        """ Initializer """
        # Call the parent class initializer
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")

        # Variables that will hold sprite lists.
        self.player_list = None
        self.coin_list = None

        # Set up the player info
        self.player_sprite = None
        self.score = 0

        # Don't show the mouse cursor
        self.set_mouse_visible(False)

        arcade.set_background_color(arcade.color.AMAZON)

    def on_draw(self):
        arcade.start_render()


def main():
    """ Main method """
    window = MyGame()
    arcade.run()


if __name__ == "__main__":
    main()

The variables we are creating:

  • player_list: When working with sprites, we normally put them into lists. Other game engines might call these sprite groups, or sprite layers. Our game will have one list for the player, and one list for the coins. Even if there is only one sprite, we should still put it in a list because there is a lot of code in SpriteList to optimize drawing.

  • coin_list: This is a list of all the coins. We will be checking if the player touches any sprite in this list.

  • player_sprite: This points to our player’s sprite. It is the sprite we will move.

  • score: This keeps track of our score.

We use a command built into the parent Window class called set_mouse_visible to make the mouse not visible. Finally we set the background color.

21.2.3. The setup Function

Next up, we will create a setup method. This will create our sprites and get our game set up. We do this in a different method than __init__ so that if we ever want to restart the game, we can just call setup again.

The setup method is not called automatically. Therefore in the example below, note we have added the code that calls the setup function near the end: window.setup().

Sprite Sample With Player
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
""" Sprite Sample Program """

import random
import arcade

# --- Constants ---
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_COIN = 0.2
COIN_COUNT = 50

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class MyGame(arcade.Window):
    """ Our custom Window Class"""

    def __init__(self):
        """ Initializer """
        # Call the parent class initializer
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")

        # Variables that will hold sprite lists
        self.player_list = None
        self.coin_list = None

        # Set up the player info
        self.player_sprite = None
        self.score = 0

        # Don't show the mouse cursor
        self.set_mouse_visible(False)

        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up the game and initialize the variables. """

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

        # Score
        self.score = 0

        # Set up the player
        # Character image from kenney.nl
        self.player_sprite = arcade.Sprite("character.png", SPRITE_SCALING_PLAYER)
        self.player_sprite.center_x = 50
        self.player_sprite.center_y = 50
        self.player_list.append(self.player_sprite)

    def on_draw(self):
        arcade.start_render()

        # Draw the sprite lists here. Typically sprites are divided into
        # different groups. Other game engines might call these "sprite layers"
        # or "sprite groups." Sprites that don't move should be drawn in their
        # own group for the best performance, as Arcade can tell the graphics
        # card to just redraw them at the same spot.
        # Try to avoid drawing sprites on their own, use a SpriteList
        # because there are many performance improvements in that code.
        self.coin_list.draw()
        self.player_list.draw()


def main():
    """ Main method """
    window = MyGame()
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

How does the code above work?

First, we need some lists to hold our sprites. We could do use a list like this:

coin_list = []

But wait! coin_list is an instance variable that’s part of our class. we need to prepend it with self..

self.coin_list = []

However, the Arcade library has a class especially for handling sprite lists. This class is called SpriteList. For more information, check out the arcade.SpriteList documentation. So instead of creating an empty list with [], we will create a new instance of the SpriteList class:

self.coin_list = SpriteList()

Except that doesn’t work. Why? SpriteList is in the Arcade library. We need to prepend any reference to things in the Arcade library with arcade of course, so now we have:

self.coin_list = arcade.SpriteList()

We need a separate list for just coins. This list won’t have the player. We also need to reset our score to 0.

self.coin_list = arcade.SpriteList()

self.score = 0

Now we need to create our sprites. The name of the class that represents sprites is called Sprite. You can read more about it by looking at the arcade.Sprite documentation. The Sprite constructor takes two parameters. A path to the image we will be using, and how big to scale it.

For class, please source the image right before you load it. If you drew your own image, please note that as well.

# Character image from kenney.nl
self.player_sprite = arcade.Sprite("character.png", SPRITE_SCALING_PLAYER)

How do we draw all our sprites? Really easy. Just call the draw method that exists for us in the SpriteList class. We just need to do this for each of our sprite lists.

def on_draw(self):

        arcade.start_render()

        # Draw all the sprite lists.
        self.coin_list.draw()
        self.player_list.draw()

Wait. We don’t have many sprites. There are no coins, and we have just the player. Let’s add a for loop to our program and create a bunch of coins:

Sprite Sample With Player And Coins
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
""" Sprite Sample Program """

import random
import arcade

# --- Constants ---
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_COIN = 0.2
COIN_COUNT = 50

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class MyGame(arcade.Window):
    """ Our custom Window Class"""

    def __init__(self):
        """ Initializer """
        # Call the parent class initializer
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")

        # Variables that will hold sprite lists
        self.coin_list = None
        self.player_list = None

        # Set up the player info
        self.player_sprite = None
        self.score = 0

        # Don't show the mouse cursor
        self.set_mouse_visible(False)

        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up the game and initialize the variables. """

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

        # Score
        self.score = 0

        # Set up the player
        # Character image from kenney.nl
        self.player_sprite = arcade.Sprite("character.png", SPRITE_SCALING_PLAYER)
        self.player_sprite.center_x = 50
        self.player_sprite.center_y = 50
        self.player_list.append(self.player_sprite)

        # Create the coins
        for i in range(COIN_COUNT):

            # Create the coin instance
            # Coin image from kenney.nl
            coin = arcade.Sprite("coin_01.png", SPRITE_SCALING_COIN)

            # Position the coin
            coin.center_x = random.randrange(SCREEN_WIDTH)
            coin.center_y = random.randrange(SCREEN_HEIGHT)

            # Add the coin to the lists
            self.coin_list.append(coin)

    def on_draw(self):
        """ Draw everything """
        arcade.start_render()

        # Draw the sprite lists here. Typically sprites are divided into
        # different groups. Other game engines might call these "sprite layers"
        # or "sprite groups." Sprites that don't move should be drawn in their
        # own group for the best performance, as Arcade can tell the graphics
        # card to just redraw them at the same spot.
        # Try to avoid drawing sprites on their own, and not in a layer.
        # There are many performance improvements to drawing in a layer.
        self.coin_list.draw()
        self.player_list.draw()


def main():
    """ Main method """
    window = MyGame()
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

21.2.4. Drawing The Score

In addition to drawing the sprites, let’s go ahead and put the score on the screen:

# Put the text on the screen.
output = "Score: " + str(self.score)
arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)

Rather than do that "Score: " + str(self.score) it is possible to use string formatting . We’ll talk more about this in the String Formatting chapter. That code would look like:

# Put the text on the screen.
output = f"Score: {self.score}"
arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)

21.2.5. The On Mouse Motion Method

Moving the player sprite with the mouse is easy. All sprites have instance variables center_x and center_y. Just change those values to the mouse’s x and y location to move the sprite.

def on_mouse_motion(self, x, y, dx, dy):

    self.player_sprite.center_x = x
    self.player_sprite.center_y = y

Now, our whole program looks like:

Sprite Sample With Mouse Motion And Score
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
""" Sprite Sample Program """

import random
import arcade

# --- Constants ---
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_COIN = 0.2
COIN_COUNT = 50

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class MyGame(arcade.Window):
    """ Our custom Window Class"""

    def __init__(self):
        """ Initializer """
        # Call the parent class initializer
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")

        # Variables that will hold sprite lists
        self.player_list = None
        self.coin_list = None

        # Set up the player info
        self.player_sprite = None
        self.score = 0

        # Don't show the mouse cursor
        self.set_mouse_visible(False)

        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up the game and initialize the variables. """

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

        # Score
        self.score = 0

        # Set up the player
        # Character image from kenney.nl
        self.player_sprite = arcade.Sprite("character.png", SPRITE_SCALING_PLAYER)
        self.player_sprite.center_x = 50
        self.player_sprite.center_y = 50
        self.player_list.append(self.player_sprite)

        # Create the coins
        for i in range(COIN_COUNT):

            # Create the coin instance
            # Coin image from kenney.nl
            coin = arcade.Sprite("coin_01.png", SPRITE_SCALING_COIN)

            # Position the coin
            coin.center_x = random.randrange(SCREEN_WIDTH)
            coin.center_y = random.randrange(SCREEN_HEIGHT)

            # Add the coin to the lists
            self.coin_list.append(coin)

    def on_draw(self):
        """ Draw everything """
        arcade.start_render()
        self.coin_list.draw()
        self.player_list.draw()

        # Put the text on the screen.
        output = f"Score: {self.score}"
        arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)

    def on_mouse_motion(self, x, y, dx, dy):
        """ Handle Mouse Motion """

        # Move the center of the player sprite to match the mouse x, y
        self.player_sprite.center_x = x
        self.player_sprite.center_y = y


def main():
    """ Main method """
    window = MyGame()
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

21.2.6. The Update Method

Our update method needs to do three things:

  1. Update each of the sprites

  2. Check to see if the player is touching any coins

  3. Remove any coins colliding with the player, and update the score.

Each sprite has its own update method. This allows sprites to move and animate its images. Right now, our sprite does not have this method. But we will soon. Rather than call the update method of each sprite we have, there is an update method in each sprite list that will call update on each sprite in the list. Therefore, just calling update with our coin_list will cause all coin sprites to update.

self.coin_list.update()

How do we detect what coins are touching the player? We call the arcade.check_for_collision_with_list() method. Pass it in our player sprite, along with a list of all the coins. That function will return a list of all colliding sprites. If no sprites collide, the list will be empty.

# Generate a list of all sprites that collided with the player.
coins_hit_list = arcade.check_for_collision_with_list(self.player_sprite, self.coin_list)

What do we do with this hit_list we get back? We loop through it. We add one to the score for each sprite hit.

We also need to get rid of the sprite. The sprite class has a method called arcade.Sprite.remove_from_sprite_lists(). This method will remove the sprite from existence.

# Loop through each colliding sprite, remove it, and add to the score.
for coin in coins_hit_list:
    coin.remove_from_sprite_lists()
    self.score += 1

Here’s the whole update method put together:

Sprite Sample With Update Method
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
""" Sprite Sample Program """

import random
import arcade

# --- Constants ---
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_COIN = 0.2
COIN_COUNT = 50

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class MyGame(arcade.Window):
    """ Our custom Window Class"""

    def __init__(self):
        """ Initializer """
        # Call the parent class initializer
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Sprite Example")

        # Variables that will hold sprite lists
        self.player_list = None
        self.coin_list = None

        # Set up the player info
        self.player_sprite = None
        self.score = 0

        # Don't show the mouse cursor
        self.set_mouse_visible(False)

        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up the game and initialize the variables. """

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

        # Score
        self.score = 0

        # Set up the player
        # Character image from kenney.nl
        self.player_sprite = arcade.Sprite("character.png", SPRITE_SCALING_PLAYER)
        self.player_sprite.center_x = 50
        self.player_sprite.center_y = 50
        self.player_list.append(self.player_sprite)

        # Create the coins
        for i in range(COIN_COUNT):

            # Create the coin instance
            # Coin image from kenney.nl
            coin = arcade.Sprite("coin_01.png", SPRITE_SCALING_COIN)

            # Position the coin
            coin.center_x = random.randrange(SCREEN_WIDTH)
            coin.center_y = random.randrange(SCREEN_HEIGHT)

            # Add the coin to the lists
            self.coin_list.append(coin)

    def on_draw(self):
        """ Draw everything """
        arcade.start_render()
        self.coin_list.draw()
        self.player_list.draw()

        # Put the text on the screen.
        output = f"Score: {self.score}"
        arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)

    def on_mouse_motion(self, x, y, dx, dy):
        """ Handle Mouse Motion """

        # Move the center of the player sprite to match the mouse x, y
        self.player_sprite.center_x = x
        self.player_sprite.center_y = y

    def update(self, delta_time):
        """ Movement and game logic """

        # Call update on all sprites (The sprites don't do much in this
        # example though.)
        self.coin_list.update()

        # Generate a list of all sprites that collided with the player.
        coins_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
                                                              self.coin_list)

        # Loop through each colliding sprite, remove it, and add to the score.
        for coin in coins_hit_list:
            coin.remove_from_sprite_lists()
            self.score += 1


def main():
    """ Main method """
    window = MyGame()
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()