Balloon Ninja: Popping balloons

Back to Tutorial Contents

Adding a sword

In the previous section we created a balloon and made it rise to the top of the screen. Let’s add a sword that the player can use to pop balloons. To do this, we will need to create a file similar to our Balloon.py file, which will describe the behavior of the sword. Save the following, in a file called Sword.py:

import pygame
from pygame.sprite import Sprite

class Sword(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('sword_75px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = self.screen.get_width()/2
        self.y_position = self.image_h/2

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

If you have understood everything in the Balloon.py file, you should be able to make pretty good sense of this file. We import the pygame Sprite module, because our sword is a sprite. We let Sword know what screen it will be drawn on, and we tell it where to find the image. We get the image width and height, and we position the sword so that it is centered at the top of the screen initially. The blitme function for Sword is identical to the Balloon blitme function; it just draws the sword image centered on the current x and y position.

I found a public domain image of a sword that should work well for our game. You can right-click and save a small version of the image right here. Save it as “sword_75px.png”, and save it in your balloon_ninja directory:

A sword for our game.

A sword for our game.

Let’s modify our game file to display the sword. Make the following changes to balloon_ninja.py:

import pygame
import sys
from Balloon import Balloon
from Sword import Sword

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # Create our sword
    sword = Sword(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Draw our sword on the screen
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))

        pygame.display.flip()

run_game()

When you run this, you should see the sword appear:

A sword appears at the top of the screen.

A sword appears at the top of the screen.

Controlling the sword

Now we need to give the player a way to control the sword. We will start letting the user control the sword by responding to the mouse (or trackpad) position. Pygame has a module called mouse that has a number of useful methods. We will start with the pygame.mouse.get_pos() function, which tells us where the mouse is on our screen. Let’s put a print statement in balloon_ninja.py which will help us understand how we can use the mouse position to give our player control over the sword:

import pygame
import sys
from Balloon import Balloon
from Sword import Sword

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # Create our dagger
    sword = Sword(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)

        print pygame.mouse.get_pos()

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Draw our sword on the screen
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))

        pygame.display.flip()

run_game()

Within our event loop, we print the mouse position right after ticking our clock. (Note that we have also lightened the background, to make the sword a little more visible as well.) If we run the game and move the mouse around a bit, we can see that our game is keeping track of the mouse’s screen coordinates:

(126, 48)
(126, 48)
(115, 41)
(99, 32)
(84, 24)
(74, 21)
(68, 18)
(63, 16)
(59, 14)
(55, 13)
(50, 12)
(46, 10)
(40, 7)
(37, 5)
(35, 4)
(32, 0)
(32, 0)
(32, 0)

Note that pygame only detects mouse positions on the actual game screen. If you move the mouse off the pygame screen, the position stays constant at the point where the mouse left the screen. Let’s use the mouse position to allow the user to move the sword back and forth across the top of the screen:

import pygame
import sys
from Balloon import Balloon
from Sword import Sword

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # Create our dagger
    sword = Sword(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update the sword's x-position, and draw the sword on the screen
        sword.x_position = pygame.mouse.get_pos()[0]
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))

        pygame.display.flip()

run_game()

We can do this in one line! We use the x-value of the mouse position to update the sword’s x_position right before we draw the sword to the screen. When you run the program now, the sword moves back and forth horizontally, with your mouse movement. Note that we have also removed the print statement from the screen.

Popping balloons

Later on we will let the player “grab” the sword, but for now we need to make the balloon disappear if the sword hits it. To do this, we need to think about what game developers call “collision detection”. Basically, we want to know when our sword “collides” with a balloon. Pygame does this, in the simplest way, by thinking of objects as rectangles. So we need to define a “rect” parameter for our sprites. Let’s take care of Balloon.py first:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = 0.1

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height() + self.image_h/2

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

All we really want to do is give our Balloon objects a “self.rect” attribute. But we also need to make sure this rect attribute is always updated to reflect the balloon’s current position. So we create a function in lines 28-30, which defines a pygame.Rect object using the current x and y positions. Pygame Rect objects are defined by the upper left corner of the rectangular area we are interested in, and the width and height of the rectangle. Since the balloon’s x and y positions are at the center of the balloon, we need to deal with half-widths and half-heights again. The upper left of our rectangle needs the x-position minus half the balloon’s image width, and the y-position minus half the balloon’s image height.

We need to update the Balloons self.rect attribute any time a balloon is created, and any time its position changes. So we call our new update_rect function when we create a balloon, and when we update a balloon.

We need to do something similar for our Sword.py file:

import pygame
from pygame.sprite import Sprite

class Sword(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('sword_75px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = self.screen.get_width()/2
        self.y_position = self.image_h/2

        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

The sword does not move on its own, so in this file we only call the update_rect function when a sword is initially created. But we will call it from within balloon_ninja.py whenever the player moves the sword with the mouse. Let’s look at the code for balloon_ninja.py. We need to update the sword’s rectangle whenever it is moved, and we need to check for collisions for each balloon:

import pygame
import sys
from Balloon import Balloon
from Sword import Sword

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # Create our dagger
    sword = Sword(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update the sword's x-position, and draw the sword on the screen
        sword.x_position = pygame.mouse.get_pos()[0]
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                balloons.remove(balloon)
                balloons.append(Balloon(screen))
                continue

            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))
                continue

            balloon.blitme()

        pygame.display.flip()

run_game()

In line 36, we update the sword.rect attribute after we move the sword. In lines 43-46, we check whether the current balloon is in contact with the sword. Pygame does this by checking to see if the balloon’s rectangle has collided with the sword’s rectangle, in line 43. If it has we remove the balloon from the list, just like we did earlier when a balloon disappeared off the top of the screen. We then create a new balloon and continue at the beginning of the loop, because there is no need to redraw a balloon that has been removed from the game. We will add a scoring function to this test a little later.

We make one other slight adjustment at this point. We move the call to blitme after our two tests, and add a continue statement to the code testing whether a balloon has disappeared off the top of the screen. We only call the blitme function if the balloon has not popped or disappeared off the top of the screen.

You may notice, if you watch carefully, that using the pygame.rect attribute creates slightly inaccurate behavior. The balloon is a round image, but it is represented by a rectangle. So you can pop a balloon by bringing the tip of the sword near the balloon, without actually touching the balloon. There are more advanced ways to use pygame, where this does not happen. The effect is hardly noticeable, though, and for many games we are fine to just go on using the sprite.rect.colliderect function.

Grabbing the sword

The next section will focus on game logic, but let’s add one more feature in this section. Let’s give the player the ability to “grab” the sword, and sweep it across the screen to pop a bunch of balloons. We will do this by checking to see whether the user has clicked on the sword. If so, we will draw the sword wherever the mouse is, as long as the mouse button is pressed. When the player lets go, the sword will return to the top of the screen. To do this, we first need to modify Sword.py:

import pygame
from pygame.sprite import Sprite

class Sword(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('sword_75px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = self.screen.get_width()/2
        self.y_position = self.image_h/2

        self.grabbed = False

        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

We have added one line, an attribute to keep track of whether the sword has been grabbed or not. It is false by default

Now we need to modify the balloon_ninja file to use this attribute effectively:

import pygame
import sys
from Balloon import Balloon
from Sword import Sword

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # Create our dagger
    sword = Sword(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if sword.rect.collidepoint(mouse_x, mouse_y):
                    sword.grabbed = True
            if event.type == pygame.MOUSEBUTTONUP:
                sword.grabbed = False

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                balloons.remove(balloon)
                balloons.append(Balloon(screen))
                continue

            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))
                continue

            balloon.blitme()

        pygame.display.flip()

run_game()

In line 25 we get the mouse’s x and y coordinates, since we will be using both now. Starting on line 31, we watch for mouse events. When the mouse button is pressed, the MOUSEBUTTONDOWN event is fired. When this happens, we check to see if the mouse position has collided with the sword’s rect object (line 32). If it has, we set the sword’s “grabbed” attribute to True. In lines 34 and 35, we set the grabbed attribute back to False as soon as the mouse button is released.

The sword’s x-position is always controlled by the mouse, so line 41 is always executed. But the sword’s y-position depends on whether the sword is grabbed or not. If it is, then it’s y-position corresponds with the mouse position. If it is not grabbed, the sword is positioned at the top of the screen.

When you run the game now, you should be able to grab the sword and swish it around at the balloons that rise up.

Cleaning our code

To wrap up this section, we will clean up our main file a bit. Let’s clean up our imports, and moving our event-checking to a separate function:

import pygame, sys
from Balloon import Balloon
from Sword import Sword

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # Create our dagger
    sword = Sword(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        check_events(sword, mouse_x, mouse_y)

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                balloons.remove(balloon)
                balloons.append(Balloon(screen))
                continue

            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))
                continue

            balloon.blitme()

        pygame.display.flip()

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

In line 1, we consolidate some of our imports. In line 27, we call our new check_events function. In lines 59-67, we define our check_events function. We will do more of this kind of cleanup before adding more game logic, but first let’s add a scoreboard to the game.

Next: Adding a scoreboard

Back to Tutorial Contents

Advertisements
Posted in programming | Tagged , , , , , | Leave a comment

An Introduction to 3d Modeling with openSCAD – openSCAD Basics

I have been introducing some of my math students to 3d modeling using the openSCAD software package.  The openSCAD documentation is really good, but can be intimidating to new users.  This is a summary of some of the basic shapes and functions that will get you started using openSCAD.

Contents

Show Your Axes
Cubes and Translation
Parameters
Spheres
Cylinders
Difference
Modules
Other openSCAD Features

Show Your Axes

Before you begin modeling, go to View>>Show Axes. This will make it easier to see where the parts of your model are being placed.

An empty openSCAD window, with axes shown.

An empty openSCAD window, with axes shown.

Cubes and Translation

A “cube” in openSCAD simply refers to a rectangular prism.  To make a simple cube:

cube(5);
A simple cube in openSCAD.

A simple cube in openSCAD.

If this is your first time using openSCAD, there are a few things you should know about the interface:

  • To see your shape once you have entered the code, go to View>>Compile (F5).
  • To zoom in on the shape, click on the drawing window and go to Edit>>Zoom In (Ctrl-+).
  • To zoom out on the shape, go to Edit>>Zoom Out (Ctrl–).
  • To view the shape from different angles, click on the drawing window and move your pointer.
  • To move the origin of your axes, right-click the drawing window and move your pointer.

You can center your cube on the xyz axes:

cube(5, center=true);
A simple cube in openSCAD, centered on the coordinate system.

A simple cube in openSCAD, centered on the coordinate system.

You can create a “cube” with different side lengths, by giving three separate parameters inside square brackets.  The numbers in brackets refer to the size of the cube in the x, y, and z directions respectively:

cube([2,2,5], center=true);
A "cube" in openSCAD actually refers to any rectangular prism.

A “cube” in openSCAD actually refers to any rectangular prism.

To move a cube anywhere you want, we use openSCAD’s translate function. The values passed to the translate function move the cube in the x, y, and z directions respectively. The following code will move our code 2 units in the x-direction, 4 units in the y-direction, and 6 units in the z-direction:

translate([2,4,6]){
    cube([2,2,5], center=true);
}
A cube that has been moved 2 units in the x direction, 4 units in the y direction, and 6 units in the z direction.

A cube that has been moved 2 units in the x direction, 4 units in the y direction, and 6 units in the z direction.

You can pass negative values to translate. The following code will move the cube 6 units down, instead of up:

translate([2,4,-6]){
    cube([2,2,5], center=true);
}
Giving translate a negative number moves it in the opposite direction.  For example, a negative z-value moves the object down.

Giving translate a negative number moves it in the opposite direction. For example, a negative z-value moves the object down.

Intermission: Make something!

At this point, it is probably good to create a model or two using nothing more than translated cubes. Sketch a simple shape using just cubes, and try to create your shape in openSCAD. You might make a tower with a door and two windows, a smiley face with a cube for a head, two eyes, and a mouth.

Parameters

You can play with openSCAD using just numbers, and things will work. But when you want to make adjustments to your models, you will probably find yourself adjusting a whole bunch of numbers. openSCAD addresses this by letting you define parameters. Let’s make a letter L, using just numbers:

// Simple letter L, with just numbers

// Vertical leg
translate([0,0,5]) {
	cube([2,2,10], center=true);
}

// Horizontal leg
translate([3,0,0]) {
	cube([8,2,2], center=true);
}
A letter L, using just numbers.

A letter L, using just numbers.

This works, but it is pretty hard to make revisions. If we want to change our model, we have to change several numbers. For example, if you want to change the cross-sectional size of the legs, you will have to change 4 numbers, and even then you still have to go back and fix the lengths to match the new cross-section:

// Simple letter L, with just numbers

// Vertical leg
translate([0,0,5]) {
	cube([4,4,10], center=true);
}

// Horizontal leg
translate([3,0,0]) {
	cube([8,4,4], center=true);
}
Changing the cross-section of the legs is not straightforward using just numbers.

Changing the cross-section of the legs is not straightforward using just numbers.

Let’s see how this same model looks using parameters:

// Simple letter L, with just numbers

// Parameters
vertical_leg_length = 10;
cross_section_width = 0.2*vertical_leg_length;
horizontal_leg_length = 0.8*vertical_leg_length;

// Vertical leg
translate([0,0,vertical_leg_length/2]) {
	cube([cross_section_width, cross_section_width, vertical_leg_length], center=true);
}

// Horizontal leg
translate([horizontal_leg_length/2-cross_section_width/2,0,0]) {
	cube([horizontal_leg_length, cross_section_width, cross_section_width], center=true);
}

Working with parameters can require a little more thinking up front, but leads to a model that is much easier to refine later. In line 4, we define our core parameter, the length of the vertical leg of the letter L. In line 5, we define our cross-sectional width in terms of the vertical leg length. In line 6, we make the horizontal leg 80% of the length of the vertical leg.

In line 9, we move the vertical leg up half of its length, so it sits on the x-y plane. The numbers in line 14 take a little more thought. We move the horizontal leg to the right by half the length of the horizontal leg. But then we have to shift it back left by half of the cross-sectional width, to line up with the left edge of the vertical leg. If these numbers are confusing in the model you are creating, you can still use guess-and-check to work things out. Here is the model, which is identical to the one we just made:

A letter L, using parameters.

A letter L, using parameters.

But now if we want to change the cross-sectional width, we just have to change one number:

// Simple letter L, with just numbers

// Parameters
vertical_leg_length = 10;
cross_section_width = 0.4*vertical_leg_length;
horizontal_leg_length = 0.8*vertical_leg_length;

// Vertical leg
translate([0,0,vertical_leg_length/2]) {
	cube([cross_section_width, cross_section_width, vertical_leg_length], center=true);
}

// Horizontal leg
translate([horizontal_leg_length/2-cross_section_width/2,0,0]) {
	cube([horizontal_leg_length, cross_section_width, cross_section_width], center=true);
}

Since we have defined all of the relationships in terms of the first parameter, everything adjusts itself for us:

With appropriate use of parameters, we can efficiently make changes to our model.

With appropriate use of parameters, we can efficiently make changes to our model.

Now let’s look at the rest of the shapes you can use in openSCAD.

Spheres

A simple sphere can be defined in one line:

// A simple sphere

sphere(5);
A simple sphere.

A simple sphere.

We can control the resolution of the sphere with the special parameter “$fn”. This setting controls the number of edges that are used to create a curve in openSCAD. You can think of this setting as “the number of faces along the equator of the sphere”. If you set $fn very high, you will get a smooth sphere but openSCAD will take a long time to render your models. It is good practice to use a moderate number, such as 20 or 40, while working, and then use a high number such as 100 or 150 for rendering your final versions.

// A simple sphere

$fn = 50;

sphere(5);
A higher-resolution sphere, with 50 faces around the equator.

A higher-resolution sphere, with 50 faces around the equator.

// A simple sphere

$fn = 150;

sphere(5);
A really high-resolution sphere, with 150 faces around the equator.

A really high-resolution sphere, with 150 faces around the equator.

The value for $fn applies to all rounded shapes in your model. If you want to have different resolutions for individual spheres, you can place the $fn setting within the call to make an individual sphere:

// A simple sphere

sphere(5, $fn=200);

Cylinders

A cylinder takes two variables to define, and responds to the $fn variable just as a sphere does. A cylinder needs a radius and a height:

// A simple cylinder

$fn = 50;

cylinder(r=3, h=5);
A simple cylinder.

A simple cylinder.

Like cubes, a cylinder is often easier to work with if it is centered first:

// A simple cylinder

$fn = 50;

cylinder(r=3, h=5, center=true);
Cylinders are often easier to work with if they are centered first.

Cylinders are often easier to work with if they are centered first.

Cylinders have a vertical orientation by default. If we want a cylinder to lie horizontally, we can use the rotate function. The rotate function, like translate, takes 3 arguments. We can choose to rotate an object about the x-axis, the y-axis, or the z-axis:

// A simple cylinder

$fn = 50;

rotate([0,90,0]) {
	cylinder(r=3, h=5, center=true);
}
A cylinder, rotated sideways about the y-axis.

A cylinder, rotated sideways about the y-axis.

If you want to move a horizontal cylinder, make sure to put the translate function around the rotate function:

// A simple cylinder

$fn = 50;

translate([0,0,5]) {
	rotate([0,90,0]) {
		cylinder(r=3, h=5, center=true);
	}
}
To move a horizontal cylinder, rotate it first and then move it.

To move a horizontal cylinder, rotate it first and then move it.

You make a cone by creating a cylinder a different radius for each end:

// A simple cylinder

$fn = 50;

cylinder(r1=5, r2=2, h=10, center=true);
A cone is just a cylinder, with a different radius for each end.

A cone is just a cylinder, with a different radius for each end.

Setting one radius to zero makes a perfect cone:

// A simple cylinder

$fn = 50;

cylinder(r1=5, r2=0, h=10, center=true);
Setting one radius to zero makes a perfect cone.

Setting one radius to zero makes a perfect cone.

Difference

Everything we have looked at so far can be used to build complex shapes by adding a bunch of smaller parts together. But sometimes we need to take pieces away. A good example of this is a ring; a ring is a cylinder, with a smaller cylinder removed from the middle. The difference function builds the first thing in the list, and removes everything else:

// A simple ring.

$fn = 50;

difference() {
	// This piece will be created:
	cylinder(r=10, h=5, center=true);

	// Everything else listed will be taken away:
	cylinder(r=8, h=6, center=true);
}
The difference function removes parts from your model.

The difference function removes parts from your model.

It is very helpful to see what is being taken away from your model. Putting a pound sign in front of the pieces that are taken away makes them visible:

// A simple ring.

$fn = 50;

difference() {
	// This piece will be created:
	cylinder(r=10, h=5, center=true);

	// Everything else listed will be taken away:
	#cylinder(r=8, h=6, center=true);
}
The pound symbol lets us see what is being taken away.

The pound symbol lets us see what is being taken away.

The piece that is being taken away must be slightly larger in one dimension than the piece it is being subtracted from. For example, this is what happens if the cylinder removed from the middle has the same height as the outer cylinder:

// A simple ring.

$fn = 50;

difference() {
	// This piece will be created:
	cylinder(r=10, h=5, center=true);

	// Everything else listed will be taken away:
	cylinder(r=8, h=5, center=true);
}
If the inner cylinder is the same height as the outer cylinder, there is a zero-thickness "skin" on the ring.

If the inner cylinder is the same height as the outer cylinder, there is a zero-thickness “skin” on the ring.

There is a zero overlap between the outer and inner cylinders, which creates a “skin” of thickness 0. This is a bad thing in modeling, and will result in errors when trying to render or upload to a 3d printer. Using parameters demonstrates a good approach to this issue:

// A simple ring.

$fn = 50;

// Parameters
inner_radius = 8;
thickness = 2;
outer_radius = inner_radius + thickness;
height = 5;
padding = 0.1;

difference() {
	// This piece will be created:
	cylinder(r=outer_radius, h=height, center=true);

	// Everything else listed will be taken away:
	#cylinder(r=inner_radius, h=height+padding, center=true);
}
A simple ring, using parameters.

A simple ring, using parameters.

If you want to build up a few things before removing some parts, you can use the union function:

// A ring with a lip at the top

$fn = 50;

// Parameters
inner_radius = 8;
thickness = 2;
outer_radius = inner_radius + thickness;
lip_radius = outer_radius + thickness;
lip_height = thickness/2;
height = 5;
padding = 0.1;

difference() {

	// These pieces will be created:
	union() {
		// main body of ring
		cylinder(r=outer_radius, h=height, center=true);

		// lip at top of ring
		translate([0,0,height/2+lip_height/2]) {
			cylinder(r=lip_radius, h=lip_height, center=true);
		}

	}

	// Everything else listed will be taken away:
	#cylinder(r=inner_radius, h=height+2*lip_height+padding, center=true);
}
A ring with a lip.

A ring with a lip.

This may seem to get complicated quickly, if you are used to a mouse-driven modeling program. But it is quite powerful to be able to write down an exact recipe for your models. We can quickly get into an efficient process of creating a model, and then making very precise refinements to our model. This approach certainly works better for some models than others.

Modules

Modules are a powerful feature of openSCAD. Once we have created a shape we like, we can give it a name. From that point on, making that shape is as simple as using the name we have given it. Let’s wrap our ring in a module:

// A simple ring.

$fn = 50;

// This line tells openSCAD to find the instructions for how to make a ring,
//  and then make the ring
ring();

// Instructions for how to make a ring
module ring() {

	// Parameters
	inner_radius = 8;
	thickness = 2;
	outer_radius = inner_radius + thickness;
	height = 5;
	padding = 0.1;

	difference() {
		// This piece will be created:
		cylinder(r=outer_radius, h=height, center=true);

		// Everything else listed will be taken away:
		#cylinder(r=inner_radius, h=height+padding, center=true);
	}

}
A simple ring, using parameters.

The same ring we saw earlier.

This is the same ring we saw earlier. We can improve this, by allowing the ring module to accept a few parameters. Let’s have the user pass in the inner radius, the thickness, and the height:

// A simple ring.

$fn = 50;

// This line tells openSCAD to find the instructions for how to make a ring,
//  and then make the ring
ring(8,2,5);

// Instructions for how to make a ring
module ring(inner_radius, thickness, height) {

	// Parameters
	outer_radius = inner_radius + thickness;
	padding = 0.1;

	difference() {
		// This piece will be created:
		cylinder(r=outer_radius, h=height, center=true);

		// Everything else listed will be taken away:
		#cylinder(r=inner_radius, h=height+padding, center=true);
	}

}

Now we can make a ring any size we want, we can easily make any number of rings we want, and we can use any of the functions we have learned earlier on our rings. I will just demonstrate this by rotating and translating a ring:

// A simple ring.

$fn = 50;

translate([0,0,15]) {
	rotate([0,90,0]) {
		ring(8,2,5);
	}
}

// Instructions for how to make a ring
module ring(inner_radius, thickness, height) {

	// Parameters
	outer_radius = inner_radius + thickness;
	padding = 0.1;

	difference() {
		// This piece will be created:
		cylinder(r=outer_radius, h=height, center=true);

		// Everything else listed will be taken away:
		cylinder(r=inner_radius, h=height+padding, center=true);
	}

}
Using modules makes it much easier to rotate and translate complex objects.

Using modules makes it much easier to rotate and translate complex objects.

Modules are indispensable when you are working with a feature that appears multiple times in your overall model.

Other openSCAD Features

If you have understood most of what has been described here, then you can probably make sense of the rest openSCAD’s features using the project’s documentation. Here are a few things to look at, which build on what was described here:

  • Comments
    • Comments are useful for writing notes in English about the parts of your model.  They can also be used to hide parts of your model, so you can focus on certain details.  To hide a piece, highlight the code for that piece and go to Edit>>Comment (Ctrl-D).  When you compile your model, that part will disappear.  To show it again, highlight the code and choose Edit>>Uncomment (Ctrl-Shift-D).
  • Scale
    • You can wrap any block of code in the scale function, and openSCAD will scale that part of your model.  You can make things larger or smaller.  This is no substitute for parameters, however, because it scales everything in your model by the same factor.
  • Color
    • You can wrap any part of your model in the color function, and bring color into your models.  OpenSCAD uses a decimal color model, where “color([0,0,0]){}” is black, and “color([1,1,1]){}” is white.  You can use any value from 0 to 1.  The values represent red, green, and blue respectively.  Keep in mind that many 3d printers do not use this color information.
  • For Loop
    • The for loop can be used to automate the creation of similar parts.  For example, if you wanted a series of spheres on the outside of your ring, spaced 45 degrees apart, you could use a for loop to create these spheres as a group.
  • Intersection
    • The union function combines two objects into one; the intersection finds only the areas that overlap between two objects.
  • Animation
    • There is a special variable called $t.  The value of $t increases with time.  So if you use $t to define the size of a cube, that cube will grow over time.  You can use simple trigonometric functions to create cyclic behavior in your models.
  • Import
    • You can define a module in another file, and then import that file into your main project file.  This is useful for isolating parts of your model, and keeping yourself from dealing with very large files.  It also means you can reuse parts in different projects, share parts with other designers, and use parts that other designers have created.
Posted in modeling, openSCAD | Tagged , , | 10 Comments

Tsunami Evacuation, January 5 2013

Last night my family experienced the biggest earthquake any of us have ever felt.  We live in Sitka, Alaska, and we were roused just before midnight by a magnitude-7.5 earthquake just over 100 miles away.  I learned how to plot earthquake activity last month, so I thought I’d take a look at last night’s events.

First of all, let’s look at last night’s quake in the context of all the world’s earthquakes over the last 7 days.  The following map shows every earthquake that has occurred in the last 7 days, with a magnitude of 1.0 or greater:

All the world's earthquakes in the last 7 days.  The quake we felt is the large red dot on the eartern Gulf of Alaska.

All the world’s earthquakes in the last 7 days. The quake we felt is the large red dot on the eartern Gulf of Alaska.

On this map, earthquakes of magnitude 5.0 or greater are shown in red.  The quake we felt was the large red dot in the eastern Gulf of Alaska.  Clearly, it is the biggest earthquake in the world in the last week.

Our son Ever, who is almost two years old, woke up just before the quake hit.  I had gone in to see if he needed anything, and when I put my hand on the rail of his crib and leaned over to check on him I suddenly felt dizzy.  I thought I had just gotten up too fast, but when I stood up I realized the house was moving!  So I scooped him up and stood in the doorway, just as Erin came out to see if we were okay.

We all stood in the doorway for about a minute, and when the shaking was over we took a few minutes to figure out what to do.  We pretty quickly grabbed a few things and headed to the local high school, which serves as a tsunami evacuation center.  We weren’t sure what to expect as we got in the car.  It was surreal, with the tsunami sirens going off and a deep voice booming through the sky saying something like, “A tsunami may be imminent.  Go to higher ground.  Do not ignore this message.”  We felt like they should have just gone ahead and added another line, something like, “You are all going to die.  Please do not panic.”  People were driving erratically, and I was more scared of getting in a car accident than a tsunami.

Ever and a friend watching people arrive at the evacuation center.

Ever and a friend watching people arrive at the evacuation center.

We made it to the evacuation center, and waited things out with a bunch of people we know. It was funny to see all the kids in their pajamas, half of them rubbing their eyes looking sleepy, and half of them excited to see everyone at such an unexpected time.   We waited anxiously for the first wave to hit, which was scheduled to arrive at 12:43 am.  Nothing significant came of it, and we all wondered when we would get to go home.  There were reports of aftershocks, and someone reported a 6-inch surge in a town 50 miles south of here.

I was curious about those aftershocks, so I made a plot of just our region.  It turns out there were quite a number of aftershocks, and they were not all minor.  That one dot on the world map obscures all the aftershocks:

There were a whole bunch of aftershocks, and they were not all minor.

There were a whole bunch of aftershocks, and they were not all minor.

We finally got the all-clear around 3am, and headed home in an uneventful but still slightly eerie atmosphere. Everyone in town seems to have slept in this morning and seems a little more relaxed after our brush with seismic disaster.

Posted in family | Tagged , , , , | Leave a comment

Balloon Ninja: Releasing balloons

Back to Tutorial Contents

Showing a balloon

In the previous section we created an empty game window, and gave the window a background color. Now let’s add a balloon to the screen.

First we need to describe what a balloon is.  This is a class file, so save the following code as “Balloon.py”, with a capital B. Save it in your “balloon_ninja” directory.

import pygame
from pygame.sprite import Sprite

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = 0
        self.y_position = 0

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position, self.y_position)
        self.screen.blit(self.image, draw_pos)

We have just created a class called “Balloon”. This file does not do anything by itself; it just defines what a balloon is, and what a balloon can do. Let’s look at what this says about a balloon. Every class needs an __init__ function, which will be called when we ask Python to create a balloon. For now our balloon only needs one piece of information – it needs to know what screen it will draw itself on. The word “self” refers to the current balloon that is being dealt with. If you are new to classes, don’t worry about this much for now. Basically, it will allow us to create a bunch of separate balloons using the same code, and each balloon will be able to refer to “itself”.

Our balloon is really just an image. We need to tell pygame where to find our balloon image. I started with a larger balloon image and resized it, but you can right click and save a small version of the balloon I used. You can use any image you want; if you wanted to change this game to “guido_ninja.py”, you could use this image. Just make sure the name of the file you save matches the name of the file in your Balloon.py code, on line 11.

The balloon we are starting out with.

The balloon we are starting out with.

In line 12 we store the width and height of our balloon image. We will use these values to draw the balloon centered on the point we are interested in. Without these values, the balloon image would be drawn with one of its corners at whatever point we specify. In lines 14 and 15, we make the balloon start out at the screen coordinates 0,0. This is the upper left position, as we will see in a moment. If you are new to screen coordinates, it is useful to note that the y-axis has its zero at the top of the screen, where we tend to start reading. This is also a useful convention in that we always have a top of a window, but many windows, such as browsers, can extend off the screen at the bottom.

The blitme function is critical to understanding how to use Pygame. The word “blit” refers to redrawing an object to the screen. Good game animation is all about managing how, when, and where you draw objects that have changed. Pygame manages most of this for us, and the blitme function is where we specify what needs to be redrawn. In this case, we tell the balloon to draw itself at its current x and y position. The screen.blit applies this drawing to our screen.

The beauty of classes is that we can now create as many balloons as we want! We will start by creating just one, but we will soon be creating a whole bunch of balloons, from this small code block. Let’s add some code to our balloon_ninja.py file, to make a balloon appear on our screen:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)

    # Create our first balloon
    new_balloon = Balloon(screen)

    # main event loop
    while True:

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Draw our balloon on the screen
        new_balloon.blitme()

        pygame.display.flip()

run_game()

You should see a balloon appear in the top left corner of the screen:

A balloon appears in the top left of the screen, at screen coordinates 0,0.

A balloon appears in the top left of the screen, at screen coordinates 0,0.

Positioning a balloon

We want to make a few changes to our Balloon.py code.  First of all, we want our balloons to appear at the bottom of the screen and then rise up.  The bottom of the screen has a y-value equal to the height of our screen.  Let’s change the initial y-value of our balloon to the bottom of the screen:

import pygame
from pygame.sprite import Sprite

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = 0
        self.y_position = self.screen.get_height()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position, self.y_position)
        self.screen.blit(self.image, draw_pos)

The screen object has a method that can give us the current height of the screen, so in line 14 we use this method to place our balloon at the bottom of the screen. But when we run balloon_ninja.py, we don’t see a balloon anymore. This is because the balloon is drawn with its top left corner at the position we specify. Let’s make a small adjustment to our balloon’s drawing function, so that the balloon is centered at its current position:

import pygame
from pygame.sprite import Sprite

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = 0
        self.y_position = self.screen.get_height()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

Now in line 17 we draw the balloon at its current position, but it is shifted left by half of its width and up by half of its height. This centers the balloon at its current x and y coordinates:

Now the balloon appears centered at the bottom left of the screen.

Now the balloon appears centered at the bottom left of the screen.

This is better; the balloon is almost appearing where we want it to. So, where do we really want our balloons to appear? We want them to appear at a random x-position, and then rise up. The leftmost position a balloon can appear at should make the balloon’s left edge match up with the left edge of the screen, and the rightmost possible position should have the balloon’s right edge flush with the right edge of the screen. Here is the code to make our balloon appear at a random x-position:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

In line 14 we get a random x position for the balloon. We want the smallest x-value to be half of the balloon’s width. The greatest value should be the width of the screen, minus half of the balloon’s width. Now if we run balloon_ninja.py we should see something like this:

Now the first balloon appears in a random position at the bottom of the screen.

Now the first balloon appears in a random position at the bottom of the screen.

Making the balloon rise

Let’s make our balloon rise. To do this, we need to add some code to our main game file, and to the Balloon file. The Balloon file needs an update function, which will decrease the balloon’s y-value every time it is called. Our balloon objects also need a speed setting:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = 0.1

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

The main file needs a pygame clock to keep track of time, and a call to the balloon’s update function just before it is drawn in the main event loop:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create our first balloon
    new_balloon = Balloon(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloon, and draw it on the screen
        new_balloon.update(time_passed)
        new_balloon.blitme()

        pygame.display.flip()

run_game()

If you run this code, you will see a balloon start out at a random position at the bottom of the screen, and rise up to the top of the screen. But if we put a print statement into our main game file, we can see that this balloon actually rises forever! To see this, add a print statement just after our call to new_balloon.update:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create our first balloon
    new_balloon = Balloon(screen)

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloon, and draw it on the screen
        new_balloon.update(time_passed)
        print "y_position: ", new_balloon.y_position
        new_balloon.blitme()

        pygame.display.flip()

run_game()

The output of this statement shows that the balloon continues to rise past the top of the screen, until our machine runs out of memory:

y_position:  4.9
y_position:  2.9
y_position:  0.9
y_position:  -1.1
y_position:  -3.1
y_position:  -5.1
...
y_position:  -1462.7
y_position:  -1464.7
y_position:  -1466.7
...

Refining our balloons

We are going to fix this by creating a list to store our balloons in. We will also watch our balloon, and remove it from the game when it disappears off the top of the screen. Make the following changes to balloon_ninja.py:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            print "y_position", balloon.y_position
            balloon.blitme()
            if balloon.y_position < 0:
                balloons.remove(balloon)

        pygame.display.flip()

run_game()

In lines 15 and 16, we create a list to hold our balloons. When we create this list, we also create our first balloon in the list. In lines 30-36, we loop through our list of balloons. We update each balloon, and use each balloon’s blitme function to redraw it on our screen. We check to see if the balloon has crossed the top of the screen, and if it has we remove it from our list. When we begin implementing our game logic, this is where we will keep track of how many balloons the player has missed. We leave the print statement in for the moment, so we can verify that our balloons are no longer rising to infinite heights in the sky. When you run the program, you should see the y-values stop once they reach 0:

y_position 5.3
y_position 3.3
y_position 1.3
y_position -0.7
$

There is a slight improvement we should make to our balloon’s behavior. If you watch closely you will see that when the balloon first appears on the screen, it does not actually rise from below the screen. It appears with the top half of the balloon immediately visible. This is because the initial y-value of the balloon is equal to the screen height. We need to adjust this initial position down by half of the balloon’s height in Balloon.py:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = 0.1

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height() + self.image_h/2

    def update(self, time_passed):
	    self.y_position -= self.speed * time_passed

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

When you run the program now, you should see the top of the balloon start rising from the bottom of the screen. If the balloon is rising too quickly to see this, you can always change the value of self.speed in line 13 to a smaller value, such as 0.01. This would be much too slow for the actual game, but it will let you see the balloon’s initial behavior more clearly.

The same problem happens at the top of the screen. We don’t actually want to remove the balloon from our list until the bottom of the balloon has cleared the top of the screen. This happens when the center of the balloon (it’s y_position) is half the balloon’s height past the top of the screen. Here is the balloon_ninja.py code to do that (we also remove the print statement in this listing):

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)

        pygame.display.flip()

run_game()

When you run the program now, you should see a single balloon appear at a random position at the bottom of the screen, and rise smoothly past the top of the screen until it is out of sight:

A balloon rises from below the bottom of the screen, until it disappears off the top of the screen.

A balloon rises from below the bottom of the screen, until it disappears off the top of the screen.

A steady stream of balloons

Now our code is clean enough that we can do some fun things. First let’s modify our balloon_ninja.py file so that as soon as one balloon disappears off the top of the screen, a new balloon starts to rise from the bottom:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))

        pygame.display.flip()

run_game()

That is a one-line change, that makes our game go on forever! Let’s make it more interesting, so that two balloons are added every time a balloon reaches the top of the screen:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                # Create two new balloons every time a balloon disappears!
                for x in range(0,2):
                    balloons.append(Balloon(screen))

        pygame.display.flip()

run_game()

Now you should see more balloons appear every time a balloon reaches the top of the screen:

Every time a balloon disappears off the top of the screen, two new balloons start rising from the bottom of the screen.

Every time a balloon disappears off the top of the screen, two new balloons start rising from the bottom of the screen.

Our game logic will be much more interesting than this. In the next section we will add a sword at the top of the screen, which the player will use to pop balloons. For now, let’s clean up our code so that we are ready to focus on the sword. Our Balloon.py file does not need any modification, because we have the balloon behavior that we want. But let’s remove the spawning behavior in the balloon_ninja.py file:

import pygame
import sys
from Balloon import Balloon

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen)]

    # main event loop
    while True:
        time_passed = clock.tick(50)

        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)
            balloon.blitme()
            if balloon.y_position < -balloon.image_h/2:
                balloons.remove(balloon)
                balloons.append(Balloon(screen))

        pygame.display.flip()

run_game()

Next:  Popping Balloons

Back to Tutorial Contents

Posted in programming | Tagged , , , | Leave a comment

Balloon Ninja: Creating a screen

Back to Tutorial Contents

In this first section of the Balloon Ninja tutorial, we will install Pygame and create an empty screen.

Installing Pygame

If you are using ubuntu, you can install pygame in one line:

$ sudo apt-get install python-pygame

Creating a screen

Here is the bare-bones code to make a game window appear on your screen. Create a directory wherever you save your programs, and call it “balloon_ninja”. Save this code as “balloon_ninja.py”, in your new balloon_ninja directory:

import pygame

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)

run_game()

We start by importing pygame.  Then we define a function called run_game that will manage the overall game play.  This function starts by defining our screen parameters, and then initializes our game and builds the screen.  Finally, we run this function.

The pygame documentation is quite good.  If you are interested, check out the documentation for the display.set_mode function we just used.  You might also want to take a look at the pygame.Surface documentation if you are already writing your own game.  The call to pygame.display.set_mode returns a pygame Surface, which  is used for drawing our game.  Our screen is a pygame Surface.

If you run this code, you will not see much.  A black window will appear, and then disappear after a moment.  Let’s make the window stick around until we choose to close it.  Add a call to “import sys” at the beginning of your file, and then add a main event loop to the run_game function:

import pygame
import sys

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)

    # main event loop
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

run_game()
The simplest empty window created by Pygame.

The simplest empty window created by Pygame.

This sets up an infinite loop once the screen is displayed.  Within this loop, we are constantly watching for pygame “events”.  These events are actions such as moving a mouse, clicking a mouse button, pressing a key, or any other action a user might make.  The only way to break this loop is to initiate a pygame.QUIT action, which for now is just pressing the orange x in the top left corner of the game window.  When that button is pressed, we call sys.exit to quit our game.

Let’s do one simple thing to our screen before we start playing with balloons: let’s set a background color for our window.  We need to modify our game and screen parameters to include a background color.  Then we need to add some code to the event loop, which will redraw the background on every pass through the loop:

import pygame
import sys

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 150, 150, 150

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)

    # main event loop
    while True:
        # Check for game events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)
        pygame.display.flip()

run_game()

Simple game animation works by drawing on one screen, and then displaying that screen. When something changes, we draw on a hidden screen, and then display that second screen when we are finished with our new drawing. Pygame manages much of this work for us, and we see that in the screen.fill and pygame.display.flip lines. When you run this code, you should see an empty gray window:

An empty screen, which we will use to create our game.

An empty screen, which we will use to create our game.

In the next section, we will add a balloon to the screen, and make it rise to the top of the game window.

Next:  Releasing Balloons

Back to Tutorial Contents

Posted in programming | Tagged , , , , | Leave a comment

Balloon Ninja: A Simple Pygame Tutorial

Introduction

Balloon Ninja, before kittens have been introduced.

Balloon Ninja, before kittens have been introduced.

If you are learning to program in Python, writing your own game is a great way to immerse yourself in the language.  Writing a game pushes you  to understand the core parts of the language such as data structures and program flow, but also moves you quickly into using existing libraries.  This tutorial walks you through the creation of a simple game, Balloon Ninja, using the Pygame framework.

I have watched a number of new programmers try to follow along with a variety of tutorials, and this has influenced the way I currently write tutorials.  In particular, I have observed that students tend to get confused by seeing code “snippets”.  If you are an experienced programmer and you see a code snippet showing just the changes to a previously-mentioned file, you  can probably make quick sense of how the fragment fits into the larger file.  But new programmers don’t keep the context of a larger file in their heads very well.  So every time a file is modified, I list the whole file, with the modified sections highlighted.  It makes for longer pages, but screen space is cheap.  The clarity for new programmers seems worthwhile.

The tutorial is written on an Ubuntu system, but if you have Python installed on another OS you can probably follow right along.

Tutorial Contents

  • Creating a screen
    • Create an empty screen, and give it a background color.
  • Releasing balloons
    • Release a single balloon, and then release a series of balloons.
  • Popping balloons
    • Respond to user input
  • Adding a scoreboard
    • Track game statistics, and show some stats on a game scoreboard.
  • Adding game logic
    • Add logic to make the game more interesting and challenging.
  • Start screen and game over
    • The game only starts when you click Play, not when you run the program.  The game ends when you miss too many balloons.
  • A better scoring system
    • Points increase throughout the game, and the score is reported a little more cleanly.
  • Kittens!
    • So far, you can slash away at anything on the screen.  Now there are kittens, which you must avoid hitting.
  • Wrapping up
    • Posting to GitHub, and additional features that could be implemented.

Trying Balloon Ninja

The “Wrapping Up” post includes instructions for downloading the game from GitHub, and running the game from your local installation.  Feel free to run the game and poke around the code a bit before digging into the tutorial.

Next:  Creating a screen

Posted in programming | Tagged , , , , | Leave a comment

Python or Ruby: 6 Months on Hacker News

Front-page presence of Python and Ruby articles on Hacker News for the last 6 months.

Front-page presence of Python and Ruby articles on Hacker News for the last 6 months.

I teach high school math and science, and over the last couple years I have started to teach an Introduction to Programming class.  This past spring I decided to reassess whether I should continue to base this class in Python.  I wrote a script to examine the presence of Python and Ruby on the front page of Hacker News, and the results of this investigation led me to stay with Python.  The script I wrote has been running for six months now, so I decided to take a look at the results once again.

I want to make sure I continue to teach a language that gives students a solid grounding in modern programming concepts and practices, not just whatever language I know best.  I was originally prompted to reexamine my language choice after noticing a bunch of job postings for Ruby developers.  I wondered if I would be giving my students better career prospects if I started them off in Ruby.  After the original investigation, I decided to stay with Python for a variety of reasons.

Python dominates Hacker News

If we look at the hourly graph of Python and Ruby articles, we can see that Python has consistently been more present on HN than Ruby:

Front-page presence of Python and Ruby articles on Hacker News for the last 6 months.

Front-page presence of Python and Ruby articles on Hacker News for the last 6 months.

The biggest spikes in presence are Python-related articles, and overall the most present articles are Python-related.  Ruby is not disappearing, but Python articles continue to generate more interest and more discussion on HN.

The daily graph of Python and Ruby articles also shows how much more interest there is in Python.  During the initial four-week period, two of the top five articles were about Ruby.  Over the last 6 months, however, all of the top 10 articles were about Python.

The daily graph of all articles is really dense over this time frame, but it is at least interesting to look at visually.  In this representation, the most significant articles on any topic appear at the bottom of the graph each day.  We can easily see that Python articles (in green) appear more often and closer to the bottom of the graph than Ruby articles.  This graph would be more meaningful to do with a weekly grouping of articles, but I will leave that as an exercise for the one-year mark.

Conclusion

I learned enough from the original four-week investigation to be confident in basing my Introduction to Programming class on Python for the foreseeable future.  I will continue to read about trends in programming languages and be open to re-evaluation, but Python has so much momentum in so many significant fields, that it is hard to imagine it being a less worthy starting language in the near future.

I will probably revisit this project once more at the one-year mark, and play around with the visual representations one last time.

Posted in programming | Tagged , , , , , , | 2 Comments