Skip to content

Advanced Graphics with Classes

Eric Dennison edited this page Oct 5, 2015 · 11 revisions

For this tutorial, return to your ggame-tutorials repository that you forked earlier. Create a new file called tutorial4.py and paste the following code into it to get started:

from ggame import App, RectangleAsset, ImageAsset, Sprite, LineStyle, Color, Frame

SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480

# Background
black = Color(0, 1)
noline = LineStyle(0, black)
bg_asset = RectangleAsset(SCREEN_WIDTH, SCREEN_HEIGHT, noline, black)
bg = Sprite(bg_asset, (0,0))


myapp = App(SCREEN_WIDTH, SCREEN_HEIGHT)
myapp.run()

This snippet should look familiar, as it is a "cut down" version of the last tutorial that you worked on (tutorial3.py). Notice that we have removed the step function entirely. In this tutorial, we will add the step function back, but in an entirely different way!

Create a New App Class

For the first part of this tutorial, we would like to customize the behavior of the standard App class by creating an entirely new application class called MyApp that inherits its basic behavior from the standard App class.

Paste the following snippet in just before the myapp = ... line:

class SpaceGame(App):
    """
    Tutorial4 space game example.
    """
    def __init__(self, width, height):
        super().__init__(width, height)

Then cut the four lines that create the background and paste them in below the super()... line and indent them to match. Now your code should look like this:

class SpaceGame(App):
    """
    Tutorial4 space game example.
    """
    def __init__(self, width, height):
        super().__init__(width, height)
        black = Color(0, 1)
        noline = LineStyle(0, black)
        bg_asset = RectangleAsset(width, height, noline, black)
        bg = Sprite(bg_asset, (0,0))

Yes, you could have pasted this in from the get-go, but I want you to be very clear about where this code is coming from.

Things to notice about the change:

  • class SpaceGame(App): defines a new class, called SpaceGame, that inherits all of the functionality of the standard App class.
  • The next line defines the __init__ method for the class. In this case it exactly matches the arguments of the App class' version of __init__.
  • The super().__init__(... line forces the new SpaceGame class to call the standard App class' __init__ function before beginning its own initialization. Always do this if you want your new class to fully inherit the behavior of the parent class.
  • Finally, since this code initializes the game, it makes sense to place the code for creating a black background in the __init__ method of the game class.

As it stands, your program is broken. To make the new SpaceGame class take effect, we have to instantiate it instead of instantiating the App class. Change the next to last line of the program to read:

myapp = SpaceGame(SCREEN_WIDTH, SCREEN_HEIGHT)

Try running the program. You should see a black background that is 640x480 pixels.

Create a New Sprite Class

Just above your SpaceGame class definition, paste this new code:

class SpaceShip(Sprite):
    """
    Animated space ship
    """
    asset = ImageAsset("images/four_spaceship_by_albertov_with_thrust.png", 
        Frame(227,0,292-227,125), 4, 'vertical')

    def __init__(self, position):
        super().__init__(SpaceShip.asset, position)

Run your program. It should not do anything different from before. Creating a new Sprite class does not actually create any sprites. All it does is create a blueprint for making sprites.

Add a single SpaceShip sprite by adding the following line to the end of the SpaceGame __init__ method:

SpaceShip((100,100))

Now run your code. Cool. Try adding a few more SpaceShip instances:

SpaceShip((150,150))
SpaceShip((200,50))

It looks like you are building a fleet!

The code we added is very simple, but there is one line that needs some explanation:

    asset = ImageAsset("images/four_spaceship_by_albertov_with_thrust.png", 
        Frame(227,0,292-227,125), 4, 'vertical')

The asset variable is created within the class, but outside of any methods. This makes it a class attribute that will be available to all instances of the class. We used this to call the parent Sprite class __init__ method using the syntax: SpaceShip.asset. This approach allows us to create as many instances of the SpaceShip as we want, but without creating multiple assets. There is one object representing the spaceship image, but multiple objects representing the sprites.

This call to create an ImageAsset has more arguments than we used in the previous tutorial. Here's what they are about:

  • The Frame(227,0,292-227,125) argument specifies a rectangular section within the image file. If you look at the image file in Github you will notice that it actually consists of sixteen different spacecraft images, some with rocket thrust and some without. The frame arguments refer to the horizontal and vertical location of the upper left hand corner of the sub-image we want, followed by the width and height of it.
  • The 4 argument means that the asset will actually include four sub-images of the same size as the first, and...
  • The 'vertical' argument means that those four images are arranged vertically in the image file.

All of this additional information means that this asset is ready to animate! It consists of a single spaceship without thrust, and three spaceship images that include a blast of thrust. By selecting which of these frames we want to show at any given time, we can give the appearance of motion within the sprite itself.

Animate with Step!

First, let's add some attributes to the SpaceShip class. Add the following lines at the end of the __init__ method:

        self.vx = 1
        self.vy = 1
        self.vr = 0.01

These will set an initial horizontal, vertical and rotational velocity.

Then, add a step method to the SpaceShip class. This should appear after the __init__ method (but leave a space between them):

    def step(self):
        self.x += self.vx
        self.y += self.vy
        self.rotation += self.vr

This will just add the velocities to the sprite's position attributes, x, y, and rotation. These are built-in attributes of the Sprite class that were automatically inherited by the SpaceShip class.

Unfortunately, just adding a step method to a sprite class does not mean that it will be called. So we have to add a step method to the application itself. Add the following code below the __init__ method of the SpaceGame class (but leave a space between them):

    def step(self):
        for ship in self.getSpritesbyClass(SpaceShip):
            ship.step()

You are expected to add a step method to your own versions of the standard App class. This step method is automatically called with every video frame update in the game.

This method body uses a for loop to access every instance of the SpaceShip class, then calls its step method (ship.step()). Since the SpaceGame step function is called with every video frame update, this means that every SpaceShip step function will also be called with every video frame update.

Changing the Sprite Image

Now to the animation details. We want the thrust images to animate when the user presses the space key. So here are the things we have to do:

  • Listen for when the space button is pressed down.
  • Listen for when the space button is released.
  • Use the step method to change the sprite image, depending on whether the space is down or released.

First, let's add code for managing the state of the thrusting, and listen for the appropriate keys. Add the following to the end of the SpaceShip __init__ method:

        self.thrust = 0
        self.thrustframe = 1
        SpaceGame.listenKeyEvent("keydown", "space", self.thrustOn)
        SpaceGame.listenKeyEvent("keyup", "space", self.thrustOff)

Then add the thrustOn and thrustOff methods to the SpaceShip class. Add the following immediately after the SpaceShip step method:

    def thrustOn(self, event):
        self.thrust = 1
        
    def thrustOff(self, event):
        self.thrust = 0

These simple functions will keep track of whether the space key is down (thrust is 1) or up (thrust is 0).

Finally, add the following to the end of the SpaceShip step method:

        if self.thrust == 1:
            self.setImage(self.thrustframe)
            self.thrustframe += 1
            if self.thrustframe == 4:
                self.thrustframe = 1
        else:
            self.setImage(0)

If self.thrust is set to 1, it means the space button is depressed and the sprite image is set to whatever self.thrustframe is (remember we initialized it to 1 in the class __init__ method). Image number 0 is the first image, 1 is the second, and so on. The next three lines increment the thrustframe attribute, checking to see if it has gone beyond the end of our list of images (there are only three of them) and setting it back to 1 if necessary.

Finally, if self.thrust is set to 0, it means the space button is released, and we should just display the thrustless spaceship image, which is done with self.setImage(0).

Final Details

There are many more improvements to make to our game, but these are left as exercises for the student!

You may have noticed that the spaceship sprites rotate in a very strange way. This is because the default "center" of a sprite is actually its upper left corner. You can change the center by setting the fxcenter and fycenter attributes of the Sprite (or in our case the SpaceShip) class. Do this by adding this final line to the SpaceShip __init__ method:

        self.fxcenter = self.fycenter = 0.5

Run your program again and revel in its awesomeness!

Find more information about the Sprite and App classes by examining the detailed ggame documentation.


###Questions

  1. Extend the tutorial to use the 'left arrow' and 'right arrow' keys to rotate the ships left and right.
  2. Extend the tutorial to use the AWSD keys to control the ship motion.
  3. ADVANCED: Extend the tutorial to create a Blast sprite that uses the blast.png image included in the /images folder. Use the 'enter' key to create Blast sprites on the screen at random locations (or "fire" them from the spaceship sprites).
  4. ADVANCED: Extend the tutorial to animate the Blast sprite.

Complete Source for the Tutorial

"""
tutorial4.py
by E. Dennison
"""
from ggame import App, RectangleAsset, ImageAsset, Sprite, LineStyle, Color, Frame

SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480


class SpaceShip(Sprite):
    """
    Animated space ship
    """
    asset = ImageAsset("images/four_spaceship_by_albertov_with_thrust.png", 
        Frame(227,0,292-227,125), 4, 'vertical')

    def __init__(self, position):
        super().__init__(SpaceShip.asset, position)
        self.vx = 1
        self.vy = 1
        self.vr = 0.01
        self.thrust = 0
        self.thrustframe = 1
        SpaceGame.listenKeyEvent("keydown", "space", self.thrustOn)
        SpaceGame.listenKeyEvent("keyup", "space", self.thrustOff)
        self.fxcenter = self.fycenter = 0.5

    def step(self):
        self.x += self.vx
        self.y += self.vy
        self.rotation += self.vr
        if self.thrust == 1:
            self.setImage(self.thrustframe)
            self.thrustframe += 1
            if self.thrustframe == 4:
                self.thrustframe = 1
        else:
            self.setImage(0)

    def thrustOn(self, event):
        self.thrust = 1
        
    def thrustOff(self, event):
        self.thrust = 0

        
        
class SpaceGame(App):
    """
    Tutorial4 space game example.
    """
    def __init__(self, width, height):
        super().__init__(width, height)
        black = Color(0, 1)
        noline = LineStyle(0, black)
        bg_asset = RectangleAsset(width, height, noline, black)
        bg = Sprite(bg_asset, (0,0))
        SpaceShip((100,100))
        SpaceShip((150,150))
        SpaceShip((200,50))

    def step(self):
        for ship in self.getSpritesbyClass(SpaceShip):
            ship.step()


myapp = SpaceGame(SCREEN_WIDTH, SCREEN_HEIGHT)
myapp.run()

Clone this wiki locally