Asteroids


So now that we've learned about Pygame, let's write a game! We're going to write Asteroids. If you don't know what Asteroids is,this videois the original version. Since we're using Pygame, though, we can make it look much better!

Let's start by getting some pictures for the ship and asteroids. Through some Google-Fu, I was able to find these:

We can load these images in and use them as our graphics. The asteroids are cool since they're on a grid - we can use thepygame.Surface.subsurfacemethod to grab each individual asteroid, then pick a random one for each individual asteroid.

So let's start writing code!


Game Objects

One thing that we might notice is that every object in the game (Ship, Asteroids, Bullets) all move in a similar fashion - that is, they all have some velocity (the ship's velocity changes from user input, but it exists), and they all wrap around the edges when they reach an edge. So we can implement this as our baseGameObjectclass. InGameObject.py, let's make this class.

'''
GameObject.py

implements the base GameObject class, which defines the wraparound motion
Lukas Peraza, 2015 for 15-112 Pygame Lecture
'''
import pygame

class GameObject(pygame.sprite.Sprite):
    def __init__(self, x, y, image, radius):
        super(GameObject, self).__init__()
        # x, y define the center of the object
        self.x, self.y, self.image, self.radius = x, y, image, radius
        self.baseImage = image.copy()  # non-rotated version of image
        w, h = image.get_size()
        self.updateRect()
        self.velocity = (0, 0)
        self.angle = 0

    def updateRect(self):
        # update the object's rect attribute with the new x,y coordinates
        w, h = self.image.get_size()
        self.width, self.height = w, h
        self.rect = pygame.Rect(self.x - w / 2, self.y - h / 2, w, h)

    def update(self, screenWidth, screenHeight):
        self.image = pygame.transform.rotate(self.baseImage, self.angle)
        vx, vy = self.velocity
        self.x += vx
        self.y += vy
        self.updateRect()
        # wrap around, and update the rectangle again
        if self.rect.left > screenWidth:
            self.x -= screenWidth + self.width
        elif self.rect.right < 0:
            self.x += screenWidth + self.width
        if self.rect.top > screenHeight:
            self.y -= screenHeight + self.height
        elif self.rect.bottom < 0:
            self.y += screenHeight + self.height
        self.updateRect()

Let's break this code apart. First we see that we're extending theSpriteclass. This is going to give us all that awesome functionality we were talking about!

Then we have the__init__method, which takesx,y,image, andradius. These are all pretty obvious arguments. The only weird thing happening is that we're copying the image, but this is for the rotating problem. We want to keep an unrotated copy so the image doesn't keep growing!

updateRectrecomputes therectattribute given new x and y coordinates, and a rotated image.

updatedoes the wraparound motion we were talking about before. We can use therectattribute'sleft,top,right, andbottomattributes to great effect here. Awesome!

This class just implements the very basics of motion, but it's actually really helpful, because it means we don't have to do it for every other object!


Ship

So now let's make the player's ship! This is clearly going to extend the basicGameObjectclass, which means it's going to also get the awesome stuff fromSpriteas well. Let's check outShip.py:

import pygame
import math
from GameObject import GameObject

class Ship(GameObject):
    # we only need to load the image once, not for every ship we make!
    #   granted, there's probably only one ship...
    @staticmethod
    def init():
        Ship.shipImage = pygame.transform.rotate(pygame.transform.scale(
            pygame.image.load('images/spaceship.png').convert_alpha(),
            (60, 100)), -90)  # rotate -90 because ship is pointing up, but 0 = right

    def __init__(self, x, y):
        super(Ship, self).__init__(x, y, Ship.shipImage, 30)
        self.power = 1
        self.drag = 0.9
        self.angleSpeed = 5
        self.angle = 0  # starts pointing straight up
        self.maxSpeed = 20

    def update(self, keysDown, screenWidth, screenHeight):
        if keysDown(pygame.K_LEFT):
            self.angle += self.angleSpeed

        if keysDown(pygame.K_RIGHT):
            # not elif! if we're holding left and right, don't turn
            self.angle -= self.angleSpeed

        if keysDown(pygame.K_UP):
            self.thrust(self.power)
        else:
            vx, vy = self.velocity
            self.velocity = self.drag * vx, self.drag * vy

        super(Ship, self).update(screenWidth, screenHeight)

    def thrust(self, power):
        angle = math.radians(self.angle)
        vx, vy = self.velocity
        # distribute the thrust in x and y directions based on angle
        vx += power * math.cos(angle)
        vy -= power * math.sin(angle)
        speed = math.sqrt(vx ** 2 + vy ** 2)
        if speed > self.maxSpeed:
            factor = self.maxSpeed / speed
            vx *= factor
            vy *= factor
        self.velocity = (vx, vy)

Again, let's break this down.

We see thatshipImageis stored as a class attribute - this makes sense, as any Ship we create should use the same image, which means we don't have to keep loading it every time we create a new one (which probably won't happen for Ship, but it will for Asteroids!). We also store it by calling the static methodinit- this is because we can't use most of the Pygame functions until afterpygame.init()is called. We'll callShip.init()in our game'sinitmethod.

Really, the only other interesting part istimerFired. You might ask why this isn't inkeyPressed- we want smooth motion, so we'll ask our framework whether a key is down, and update velocities based on that every frame!

thrustjust uses some basic trig to distribute extra velocity in the x and y directions.


Let's Play

So now we can test out motion and see how it works! Let's start writingGame.py.

import pygame
from Ship import Ship
from pygamegame import PygameGame


class Game(PygameGame):
    def init(self):
        Ship.init()
        self.shipGroup = pygame.sprite.Group(Ship(self.width, self.height))

    def timerFired(self, dt):
        self.shipGroup.update(self.isKeyPressed, self.width, self.height)

    def redrawAll(self, screen):
        self.shipGroup.draw(screen)

Game(600, 600).run()

The only weird part here is that we don't store the ship itself, but rather a Group which the ship is part of. This is so we can callupdate,draw, and usegroupcollidedirectly on the group.

If we run this, we can move the ship around the screen! It even wraps around, because it's a Game Object. Cool!

Let's do Asteroids now.


Asteroids (The Objects, not the Game!)

What functionality do Asteroids need, on top of the GameObject class?

  • Get a random image
  • Get a random speed
  • Be able to break apart into two smaller asteroids (so we need size)

InAsteroids.py, let's start writing theAsteroidclass, which obviously extendsGameObject.

Asteroid'sinitmethod is going to be a more complicated thanShip's, because we need to split the asteroid image up. Let's check it out.

@staticmethod
def init():
    image = pygame.image.load('images/asteroids.png').convert_alpha()
    rows, cols = 4, 4
    width, height = image.get_size()
    cellWidth, cellHeight = width / cols, height / rows
    Asteroid.images = []
    for i in range(rows):
        for j in range(cols):
            subImage = image.subsurface(
                (i * cellWidth, j * cellHeight, cellWidth, cellHeight))
            Asteroid.images.append(subImage)

There are 4 rows and 4 columns in our asteroid 'grid', so we get each individual asteroid by subsampling the Surface 16 times.

Let's also addminSizeandmaxSizeclass attributes to the Asteroid class. These will define the starting size of each asteroid. Also addmaxSpeedto pick a random asteroid velocity.

class Asteroid(GameObject):
    minSize = 2
    maxSize = 4
    maxSpeed = 5
    ''' the init method from above... '''

Now let's write the rest of the methods.

def __init__(self, x, y, level=None):
    if level is None:
        level = random.randint(Asteroid.minSize, Asteroid.maxSize)
    self.level = level
    factor = self.level / Asteroid.maxSize
    image = random.choice(Asteroid.images)
    w, h = image.get_size()
    image = pygame.transform.scale(image, (int(w * factor), int(h * factor)))
    super(Asteroid, self).__init__(x, y, image, w / 2 * factor)
    self.angleSpeed = random.randint(-10, 10)
    vx = random.randint(-Asteroid.maxSpeed, Asteroid.maxSpeed)
    vy = random.randint(-Asteroid.maxSpeed, Asteroid.maxSpeed)
    self.velocity = vx, vy

def update(self, screenWidth, screenHeight):
    self.angle += self.angleSpeed
    super(Asteroid, self).update(screenWidth, screenHeight)

def breakApart(self):
    if self.level == Asteroid.minSize:
        return []
    else:
        return [Asteroid(self.x, self.y, self.level - 1),
                Asteroid(self.x, self.y, self.level - 1)]

__init__is just creating a bunch of randomly initialized variables, and picking a random asteroid image.breakApartis the interesting method here. If an asteroid is already only level 1, breaking it makes it disappear, so we return an empty list. Otherwise we return two smaller asteroids.

Now we can add asteroids into our game!

Let's update the methods in our Game class to get Asteroids in it.

def init(self):
    # old stuff still here
    Asteroid.init()
    self.asteroids = pygame.sprite.Group()
    for i in range(5):
        x = random.randint(0, self.width)
        y = random.randint(0, self.height)
        self.asteroids.add(Asteroid(x, y))

Here, we're randomly initializing 5 asteroids.

The other two methods just involve calls toself.asteroids.updateorself.asteroids.draw. I'll let you figure those out.

If we play now, we have asteroids! But running into them doesn't do anything. This is where we'll usepygame.sprite.groupcollide! In ourtimerFired, we want to check if the Ship hit any asteroids. If it did, we remove it and create a new ship back at the center. Add this code totimerFired.

if pygame.sprite.groupcollide(self.shipGroup, self.asteroids,
    True, False,  # remove the ship, but not the asteroid
    pygame.collide_circle):  # check circular collisions, not rectangular
    self.shipGroup.add(Ship(self.width / 2, self.height / 2))

Bullets

Our Bullet class is actually really small. First we need to set a velocity based on the angle that it's shot at. Then the GameObject class takes care of most of the rest! All that we have to do is despawn the bullet after it's been on the screen for too long. We can do this by calling thepygame.sprite.Sprite.killmethod.

class Bullet(GameObject):
    speed = 25
    time = 50 * 5 # last 5 seconds
    size = 10

    def __init__(self, x, y, angle):
        size = Bullet.size
        image = pygame.Surface((Bullet.size, Bullet.size), pygame.SRCALPHA)
        pygame.draw.circle(image, (255, 255, 255), (size // 2, size // 2), size // 2)
        super(Bullet, self).__init__(x, y, image, size // 2)
        vx = Bullet.speed * math.cos(math.radians(angle))
        vy = -Bullet.speed * math.sin(math.radians(angle))
        self.velocity = vx, vy
        self.timeOnScreen = 0

    def update(self, screenWidth, screenHeight):
        super(Bullet, self).update(screenWidth, screenHeight)
        self.timeOnScreen += 1
        if self.timeOnScreen > Bullet.time:
            self.kill()

This is it for bullets. Let's add them into the game and we're done!

Add the following to the correct methods:

def init(self):
    # old stuff
    self.bullets = pygame.sprite.Group()

def keyPressed(self, keyCode, mod):
    if keyCode == pygame.K_SPACE:
        ship = self.shipGroup.sprites()[0]
        self.bullets.add(Bullet(ship.x, ship.y, ship.angle))

def timerFired(self, dt):
    # other update things
    self.bullets.update(self.width, self.height)
    # other collision stuff
    for asteroid in pygame.sprite.groupcollide(self.asteroids, self.bullets,
        True, True, pygame.sprite.collide_circle):
        self.asteroids.add(asteroid.breakApart())

def redrawAll(self, screen):
    # other drawing things
    self.bullets.draw(screen)

And now we're done! Let's play the game and have some fun :)


Extras

So this game is fun and all, but there's lots of stuff we could add. Here are some suggestions:

  • Invincibility when you first spawn / respawn
  • Score
  • Lives
  • Splash Screen
  • Power ups
  • Sound (usepygame.mixer)
  • Levels
  • Online multiplayer
  • Anything else you think would be cool

Resources

results matching ""

    No results matching ""