Advanced Graphics with Classes - Runpython-IntroProgramming/Course-Syllabus GitHub Wiki

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

myapp = App()

# Background
black = Color(0, 1)
noline = LineStyle(0, black)
bg_asset = RectangleAsset(myapp.width, myapp.height, noline, black)
bg = Sprite(bg_asset, (0,0))

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):
        super().__init__()

Then cut the four lines that create the background and paste them in below the super()... line and indent them to match. Finally, change the parts that say myapp.width and myapp.height to be self.width and self.height, respectively. Now your new piece of code should look like this:

class SpaceGame(App):
    """
    Tutorial4 space game example.
    """
    def __init__(self):
        super().__init__()
        # Background
        black = Color(0, 1)
        noline = LineStyle(0, black)
        bg_asset = RectangleAsset(self.width, self.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 expects no arguments.
  • 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 from myapp = App() to myapp = SpaceGame().

Try running the program. You should see a black background.

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,65,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 (properly indented, of course):

        SpaceShip((100,100))

Now run your code. Cool. Try adding a few more SpaceShip instances at the end of your SpaceGame __init__ method:

        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,65,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,65,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 (227 and 0 pixels), followed by the width and height of it (65 and 125 pixels).
  • 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. Go back to the github repository and look at this image file to see what I mean by "four images ... arranged vertically."

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 SpaceShip class __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 SpaceShip class __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, which are built-in attributes of the Sprite class that were automatically inherited by the SpaceShip class.

If you are unsure about where to paste these code snippets, check the full listing at the end of this page.

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 customizations 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 inside 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 inside the end of the SpaceShip step method:

        # manage thrust animation
        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

class SpaceShip(Sprite):
    """
    Animated space ship
    """
    asset = ImageAsset("images/four_spaceship_by_albertov_with_thrust.png", 
        Frame(227,0,65,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
        # manage thrust animation
        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):
        super().__init__()
        # Background
        black = Color(0, 1)
        noline = LineStyle(0, black)
        bg_asset = RectangleAsset(self.width, self.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()
myapp.run()