In this third part, we have the following objectives:

  • Clean up the classes
  • Multiple planes
  • Multiple shots.
  • Generalize the game objects into a superclass
  • Display a score
  • Assign keys to shoot and play again or exit

In the version of Part II, the classes have a lot of repeated code. By analyzing each of them, we can reach a conclusion about a common behavior regarding how to draw and update the objects. A method to remove the object from the game is also used.

Every game object needs to have access to the game itself. Although we hardly use colors, I added color as a parameter, since it can be used to represent multiple players or different types of objects.

One of the things that were not clear in the previous version is the collision control. In reality, the collision control we use is quite basic and difficult to generalize. To allow collision control between objects, each object will have an x and y position on the screen, with height and width.

Thus, two objects collide if the rectangle formed by the points:

(x, y), (x + width, y), (x + width, y + height), (x, y + height)

of two objects have common points.

As different types of objects collide with each other, the collision property holds the type of object. This way, we can separate the shots from the plane itself.

A DEBUG variable has also been added to show the collision rectangle if we need to make adjustments.

    class GameObject:
        def __init__(self, game, color):
            self.game = game
            self.color = color
            self.active = True
            self.collision = BACKGROUND
            self.width = 0
            self.height = 0
            self.x = 0
            self.y = 0
            self.debug = DEBUG

If debug is active, we will draw a box around the object.

This way, it becomes easier to detect errors in the boundaries and size of the object.

        def draw(self):
            if self.debug:
                set_color(2)
                pyglet.graphics.draw(4, pyglet.gl.GL_LINE_LOOP,
                                 ('v2f', (self.x, self.y,
                                          self.x + self.width, self.y,
                                          self.x + self.width, self.y + self.height,
                                          self.x, self.y + self.height)))

The update of the properties, although present in all objects, does not have a standard behavior. We will use only pass to mark the place.

        def update(self):
            """Calculates new position, updates values each frame"""
            pass

Here is a method to give the object a chance to handle the collision with another object. This method will be called by the game whenever two objects collide.

        def collided(self, other):
            """What to do in case of collision? Receives the other object"""
            pass

And this here is the collision test itself, very simple. It only works with positive widths and heights.

        def collides(self, other):
            if self.x < other.x + other.width and self.x + self.width > other.x and \
               self.y < other.y + other.height and self.height + self.y > other.y:
                self.collided(other)
                other.collided(self)

When the collision is detected, the collided method of both objects is called.

And finally, the method to deactivate and remove the object from the game:

        def deactivate(self):
            self.active = False
            self.game.deactivate(self)

Let’s see how the Plane class turned out:

    class Plane(GameObject):
        def __init__(self, game, color=3):
            super(Plane, self).__init__(game, color)
            self.speed = (random.randint(0, 6) + 4) * RATIO
            self.y = 159 - (random.randint(0, 135) + 11)
            self.x = self.speed
            self.height = 9
            self.width = 15
            self.collision = PLANE
            self.escaped = False

        def draw(self):
            super().draw()
            y = self.y + self.height
            x = self.x
            set_color(self.color)
            pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                                 ('v2f', (x, y,
                                          x, y - 8,
                                          x + 3, y - 2,
                                          x + 12, y - 2,
                                          x + 14, y,
                                          x, y)))

        def update(self):
            self.x += self.speed
            if self.x > 265:
                self.escaped = True
                self.deactivate()

        def collided(self, other):
            if other.collision == BULLET:
                self.game.objects.append(Explosion(self.game, self.x, self.y))
                self.deactivate()

In this version, the names of the properties have already been adjusted. I no longer use plane_height, which has become just the y coordinate. In the Plane class, I added the escaped property to mark the planes that leave the screen without being destroyed, from those that are hit by missiles. Later, this information will be used to count points.

When the plane collides with a Bullet, a new type of object, Explosion, is added to the game. This way, we will have a simple effect when the plane is hit.

One of the advantages of working with objects is the ease of creating them. In this case, the explosion is just another object added to the game.

    class Explosion(GameObject):
        def __init__(self, game, x, y):
            super(Explosion, self).__init__(game, color=0)
            self.stage = 8
            self.x = x
            self.y = y

        def draw(self):
            dx = 4 * self.stage  #random.randint(1, 4)
            dy = random.randint(1, 4) * self.stage
            dx2 = random.randint(1, 4) * self.stage
            dy2 = random.randint(1, 4) * self.stage
            y = self.y
            set_color(7)
            pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                                 ('v2f', (self.x, y,
                                          self.x - dx, y + dy,
                                          self.x + dx2, y + dy2)))
            set_color(8)
            pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                                 ('v2f', (self.x, y,
                                          self.x -dx/self.stage , y + dx,
                                          self.x + dx2 * self.stage, y + dy2)))

        def update(self):
            self.stage -= 0.4
            if(self.stage < 0):
                self.deactivate()

The idea of the explosion is that yellow and red triangles of random sizes appear on the screen. Since the explosion does not move, the stage property was added to control its lifespan. In this case, every frame, we will subtract 0.4 from the stage, and when it is negative, the object will be removed from the game.

The Bullet class was also improved:

    class Bullet(GameObject):
        def __init__(self, game, position):
            super(Bullet, self).__init__(game, color=3)
            self.y = 170
            self.x = (position + 1) * 70
            self.speed = 5 * RATIO
            self.collision = 2
            self.height = 5
            self.width = 2
            self.missed = False

        def draw(self):
            super().draw()
            set_color(self.color)
            y = self.y + self.height
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2f', (self.x, y,
                                          self.x, y - 4)))
            v = 4
            a = random.randint(3, 4)
            set_color(7)
            pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                                 ('v2f', (self.x, y,
                                          self.x - 1, y + v,
                                          self.x + 1, y + v)))
            set_color(8)
            pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                                 ('v2f', (self.x, y,
                                          self.x - 0.5, y + a,
                                          self.x + 0.5, y + a)))

        def update(self):
            self.y -= self.speed
            if self.y < 0:
                self.missed = True
                self.deactivate()

        def collided(self, other):
            if other.collision == PLANE:
                self.deactivate()

The Bullet now has a rocket; the effect is interesting, although simple. When the bullet leaves the screen without hitting a plane, it is considered a miss, and the player is penalized.

The bases are also drawn as game objects. In this version, I changed the drawing.

    class Base(GameObject):
        def __init__(self, game, position):
            super(Base, self).__init__(game, color=3)
            self.y = 175
            self.x = (position + 1) * 70

        def draw(self):
            super().draw()
            set_color(self.color)
            pyglet.graphics.draw(5, pyglet.gl.GL_LINE_STRIP,
                                 ('v2f', (self.x - 5, self.y + 5,
                                          self.x - 3, self.y + 2,
                                          self.x, self.y,
                                          self.x + 3, self.y + 2,
                                          self.x + 5, self.y + 5,
                                          )))

The game class itself has been simplified:

    class Game:
        def __init__(self, parent):
            self.color = COLORS
            self.window = parent
            self.bullets_fired = 0  # bullets already fired
            self.state = "playing"
            self.pressed = False
            self.label = None
            self.objects = [Plane(self)]
            self.objects.extend([Base(self, base) for base in range(0, 3)])
            self.points = 0

            self.score_label = pyglet.text.Label("Points: 0", font_name="Times New Roman", font_size=16,
                                       x=80, y=-185 * 4, anchor_x='center', anchor_y='center')

Now we have a score counter and a score label. An object of the Plane class and the three bases are added to the initial game.

    def deactivate(self, object):
            self.objects.remove(object)
            if object.collision == PLANE:
                if object.escaped:
                    self.points -= 3
                else:
                    self.points += 5
                self.objects.append(Plane(self))
            elif object.collision == BULLET:
                if object.missed:
                    self.points -= 1

The deactivation of an object is the removal of it from the list of game objects. When it is a plane, it checks if the plane escaped the screen without being hit. If it escaped, the player loses 3 points. If it was destroyed, the player gains 5 points.

For each bullet fired that leaves the screen without hitting a plane, the player loses 1 point.

Now that we have the score displayed, we need to modify the show_message method. This should always display the score:

        def show_message(self):
            glPushMatrix()
            glScalef(0.25, -0.25, 0.25)
            if self.label:
                self.label.draw()
            self.score_label.text = "Points: {}".format(self.points)
            self.score_label.draw()
            glPopMatrix()

The glPushMatrix and glPopMatrix were used to correct the drawing of the text. Since we are using a scale of 4 with inversion of the Y-axis coordinates, the glScalef was used with 0.25 (which in multiplication is the same as 1/4 or dividing by 4) and -0.25 on the Y-axis, which divides by 4 and inverts the sign. The glPushMatrix is used to save the current matrix on the stack, so the glScalef will be used only until the glPopMatrix restores the previous parameters.

The update method has been simplified:

        def update(self):
            self.play()
            if self.state != "playing":
                if self.pressed:
                    pyglet.app.event_loop.exit()
            self.show_message()

The play method is always called. This way we can have the drawing of the game, even when displaying an end message, for example. In this version, there is no state change, as I modified the game to generate another plane whenever one is destroyed or disappears, having no end.

And the play method has also been simplified:

        def play(self):
            for object in self.objects:
                object.draw()

            self.process_bullets()
            if self.state == "playing":
                for object in self.objects:
                    object.update()

                if self.pressed:
                    self.objects.append(Bullet(self, self.bullets_fired % 3))
                    self.bullets_fired += 1
                    self.pressed = False

It simply draws all the objects in the list and calls process_bullets, which tests for collisions. If we are still playing, it calls the update of all the objects in the list, and if something was pressed, it generates a bullet. To have multiple bullets, the base that fires it is calculated by the modulo of the bullet number and the number of bases.

    def process_bullets(self):
            bullets = [object for object in self.objects if object.collision == BULLET]
            planes = [object for object in self.objects if object.collision == PLANE]

            for plane in planes:
                for bullet in bullets:
                    plane.collides(bullet)

Here is a simplification, where I take a bit of advantage of the computer :-D Using two “list comprehensions”, I filter the list of objects, looking for planes and bullets. This way, the collision check becomes simpler, as it is enough to check if the plane collided with one of the bullets.

The main game has changed little, except for the on_key_press method:

    def on_key_press(self, symbol, modifiers):
            if symbol == key.S:
                print("Score: {}".format(self.game.points))
                pyglet.app.event_loop.exit()
            elif symbol == key.R:
                self.game = Game(self)
            else:
                self.game.pressed = True

In this case, the game ends when we press the S key and restarts if we press the R key.

The complete game turned out like this:

Video:

For the next post:

  1. Sounds
  2. Images

Pyglet has not been updated for a long time. I should look for another graphics library for future posts, but I will finish the improvements of this game still with pyglet and taking advantage of OpenGL.