Creación de un videojuego con pygame

En esta entrada, vamos a ver cómo crear un videojuego con Python y pygame. El videojuego se llama Bullet dodger. El objetivo es esquivar todas las balas que se disparen para conseguir el mayor número de puntos posibles. Cada bala disparada aumenta la puntuación en 1. El personaje se maneja con el ratón, y el juego tiene un modo de pantalla completa.

Antes de comenzar a programar, debes asegurarte de que tienes todos los materiales necesarios. Solamente necesitas pygame, Python y un editor de texto o IDE con el que te sientas cómodo. Abajo tienes la bala que utilizaremos para el juego; descárgala y ubícala en la carpeta donde vayas a programar. bullet

Paso 1: Crear ventana básica

Lo primero que hay que hacer para utilizar pygame, es importarlo. Normalmente son necesarias las librerías pygame y pygame.locals. Crea un archivo de llamado main.py e importa dichas librerías.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pygame.locals import *
import pygame

Una vez importado todo lo necesario, podemos crear el videojuego. Lo primero es hacer la ventana.

pygame.init()
# Display
screen = pygame.display.set_mode((800, 600))
# Window titlebar
pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))

Hemos iniciado pygame. Después, hemos establecido la resolución a 800x600 píxeles y, por último, hemos creado el título de nuestra ventana y le hemos asignado como icono la imagen de la bala que hemos descargado. Cuando lo ejecutemos, va a aparecer una ventana negra que se cerrará inmediatamente, porque cuando termina la última instrucción el programa ya no tiene nada más que hacer y finaliza.

Para evitar esto necesitamos que el programa se ejecute hasta que el usuario decida cerrarlo. Para ello vamos a crear el bucle del juego. Dentro de ese bucle, vamos a comprobar si el usuario ha realizado alguna acción. Si la acción realizada es la de cerrar la ventana (event.type == QUIT), pygame parará y la variable running tendrá el valor False; por lo que se saldrá del bucle y se finalizará el programa.

running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
pygame.quit()

Ventana simple en pygame

A continuación os dejo el código fuente completo.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pygame.locals import *
import pygame

pygame.init()
# Display
screen = pygame.display.set_mode((800, 600))
# Window titlebar
pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
pygame.quit()

Paso 2: Crear la pantalla de inicio

Vamos a crear un archivo en el que ubicaremos algunas constantes importantes en pygame: los colores y la resolución de la ventana. El archivo se llamará global_constants.py.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Resolution
WIDTH = 800
HEIGHT = 600

# Colors
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
YELLOW = (255, 255, 12)

La anchura (WIDTH) será de 800 píxeles; la altura (HEIGHT), de 600. Los colores en pygame son una tupla de tres valores: rojo, verde y amarillo (de ahí las siglas RGB). Vamos a crear los colores negro, verde, rojo y amarillo.

Ahora que hemos creado unas variables para la resolución, podemos utilizarlas en main.py. Vamos a añadir también un tipo de fuente para escribir texto normal (un poco más adelante veremos cómo funciona el constructor pygame.font.Font).

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pygame.locals import *
import pygame

from global_constants import *

pygame.init()
# Display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
fullscreen = False
# Window titlebar
pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))

default_font = pygame.font.Font(None, 28)

Ahora vamos a crear una función para escribir texto en pygame.

def draw_text(text, font, surface, x, y, main_color, background_color=None):
    textobj = font.render(text, True, main_color, background_color)
    textrect = textobj.get_rect()
    textrect.centerx = x
    textrect.centery = y
    surface.blit(textobj, textrect)

La función la hemos llamado draw_text. Tiene como parámetros el texto que queramos escribir, el tipo de fuente, la superficie donde queremos que se escriba el texto, las coordenadas (x y y), el color de las letras y el color de fondo tras las letras (background_color=None, opcional).

Dentro de la función hemos creado el objeto de texto, con la función font.render utilizando los parámetros. Con el objeto de texto, hemos accedido a las coordenadas rectangulares y las hemos modificado con los valores de y y x. Una vez hecho esto, podemos dibujar el texto en la superficie (surface.blit(textobj, textrect)).

Ahora crearemos la función de la pantalla de inicio del juego en main.py.

def start_screen():
    pygame.mouse.set_cursor(*pygame.cursors.diamond)
    while True:
        title_font = pygame.font.Font('freesansbold.ttf', 65)
        big_font = pygame.font.Font(None, 36)
        draw_text('BULLET DODGER', title_font, screen,
                  WIDTH / 2, HEIGHT / 3, RED, YELLOW)
        draw_text('Use the mouse to dodge the bullets', big_font, screen,
                  WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
        draw_text('Press any mouse button or S when you\'re ready',
                  default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
        draw_text('Press F11 to toggle full screen', default_font, screen,
                  WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == pygame.MOUSEBUTTONDOWN:
                main_loop()
                return
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_s:
                    main_loop()
                    return
            if event.type == QUIT:
                return

Con pygame.mouse.set_cursor(*pygame.cursors.diamond), hacemos que el ratón tenga forma de diamante cuando pasas el ratón por la ventana del juego. Después creamos otros dos tipos de fuentes, con diferentes tamaños. El tipo de fuente es freesansbold.ttf, que es la fuente predeterminada de pygame. Pasar None como primer parámetro del constructor Font tiene es equivalente a pasar 'freesansbold.ttf'; el segundo parámetro hace referencia al tamaño de la fuente. La función draw_text que creamos anteriormente permite crear varios textos fácilmente. Para que se muestren los textos, debemos ejecutar pygame.display.update(). Después creamos un bucle para capturar los eventos. Si el usuario pulsa cualquier botón del ratón o la tecla S se ejecutará la función main_loop(), que aún no hemos creado. Si el usuario cierra la ventana, Python saldrá de la función.

Creemos la función main_loop. Simplemente mete el bucle del juego que creamos anteriormente dentro de la función tal que quede como abajo.

def main_loop():
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False

Ahora solo añade al debajo de todas las funciones creadas una llamada a start_screen() y otra llamada a pygame.quit() (sirve para cerrar el motor de pygame). El archivo main.py debe haberte quedado como lo muestro continuación.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pygame.locals import *
import pygame

from global_constants import *

pygame.init()
# Display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
# Window titlebar
pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))

default_font = pygame.font.Font(None, 28)

def draw_text(text, font, surface, x, y, main_color, background_color=None):
    textobj = font.render(text, True, main_color, background_color)
    textrect = textobj.get_rect()
    textrect.centerx = x
    textrect.centery = y
    surface.blit(textobj, textrect)


def start_screen():
    pygame.mouse.set_cursor(*pygame.cursors.diamond)
    while True:
        title_font = pygame.font.Font('freesansbold.ttf', 65)
        big_font = pygame.font.Font(None, 36)
        draw_text('BULLET DODGER', title_font, screen,
                  WIDTH / 2, HEIGHT / 3, RED, YELLOW)
        draw_text('Use the mouse to dodge the bullets', big_font, screen,
                  WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
        draw_text('Press any mouse button or S when you\'re ready',
                  default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
        draw_text('Press F11 to toggle full screen', default_font, screen,
                  WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == pygame.MOUSEBUTTONDOWN:
                main_loop()
                return
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_s:
                    main_loop()
                    return
            if event.type == QUIT:
                return


def main_loop():
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False

start_screen()
pygame.quit()

captura-de-pantalla-de-2016-09-11-033008

Paso 3: Crear el modo de pantalla completa

En el menú principal del juego vamos a permitir cambiar a un modo de pantalla completa pulsando F11. Para ello tenemos que crear la función toggle_fullscreen(). Antes vamos a crear una variable llamada fullscreen y vamos a darle el valor False.

# Display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
fullscreen = False

Creemos ya la función toggle_fullscreen().

def toggle_fullscreen():
    if pygame.display.get_driver() == 'x11':
        pygame.display.toggle_fullscreen()
    else:
        global screen, fullscreen
        screen_copy = screen.copy()
        if fullscreen:
            screen = pygame.display.set_mode((WIDTH, HEIGHT))
        else:
            screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
        fullscreen = not fullscreen
        screen.blit(screen_copy, (0, 0))

Si el ordenador del usuario tiene el controlador de vídeo x11, ejecutará la función pygame.display.toggle_fullscreen(). Si no dispone de ese driver la anterior función no fuencionará, así que utilizamos otro método. En este, tenemos que hacer globales las variables screen y fullscreen para poder cambiar sus valores. Hacemos una copia de la variable screen, si el valor de la variable fullscreen es False, cambiaremos a pantalla completa con screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN); en caso contrario, volveremos al modo normal. Cambiamos el valor de la variable fullscreen (si es True pasará a False; si es False, a True). La copia de la variable screen, debemos dibujarla en screen, ya que al cambiar el modo de muestra (display.set_mode) se perderán los valores antiguos.

Dememos permitir llamar a esta función pulsando la tecla F11 en la función start_screen().

for event in pygame.event.get():
    if event.type == pygame.MOUSEBUTTONDOWN:
        main_loop()
        return
    elif event.type == pygame.KEYDOWN:
        if event.key == pygame.K_s:
            main_loop()
            return
        if event.key == K_F11:
            toggle_fullscreen()
    if event.type == QUIT:
        return

Paso 4: Crear las balas

Crea un archivo llamado bullet.py.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random

from pygame.locals import *
import pygame

from global_constants import WIDTH, HEIGHT


def random_bullet(speed):
    random_or = random.randint(1, 4)
    if random_or == 1:  # Up -> Down
        return Bullet(random.randint(0, WIDTH), 0, 0, speed)
    elif random_or == 2:  # Right -> Left
        return Bullet(WIDTH, random.randint(0, HEIGHT), -speed, 0)
    elif random_or == 3:  # Down -> Up
        return Bullet(random.randint(0, WIDTH), HEIGHT, 0, -speed)
    elif random_or == 4:  # Left -> Right
        return Bullet(0, random.randint(0, HEIGHT), speed, 0)


class Bullet(pygame.sprite.Sprite):

    def __init__(self, xpos, ypos, hspeed, vspeed):
        super(Bullet, self).__init__()
        self.image = pygame.image.load('bullet.png')
        self.rect = self.image.get_rect()
        self.rect.x = xpos
        self.rect.y = ypos
        self.hspeed = hspeed
        self.vspeed = vspeed

        self.set_direction()

    def update(self):
        self.rect.x += self.hspeed
        self.rect.y += self.vspeed
        if self.collide():
            self.kill()

    def collide(self):
        if self.rect.right < 0 or self.rect.x > WIDTH:
            return True
        elif self.rect.right < 0 or self.rect.y > HEIGHT:
            return True

    def set_direction(self):
        if self.hspeed > 0:
            self.image = pygame.transform.rotate(self.image, 270)
        elif self.hspeed < 0:
            self.image = pygame.transform.rotate(self.image, 90)
        elif self.vspeed > 0:
            self.image = pygame.transform.rotate(self.image, 180)

Este es el código que necesitamos para crear balas. La función random_bullet.py sirve para crear una bala con una dirección aleatoria (hay cuatro: de arriba abajo, de derecha a izquierda, de abajo arriba y de izquierda a derecha). Podemos controlar la velocidad a través del parámetro speed. En esta función utilizamos las variables WIDTH y HEIGHT para que las balas se creen justo en el borde fuera de la ventana del juego.

Más abajo creamos la clase Bullet, que hereda de pygame.sprite.Sprite. En el constructor (__init__) llamamos al constructor de la clase padre, establecemos la imagen de la bala, las coordenadas rectangulares y la velocidad horizontal (hspeed) y vertical (vspeed). Finalmente llamamos a la función set_direction().

La función set_direction(), simplemente gira la imagen para que la imagen de la bala no de una apariencia de movimiento equivocada. Es decir, si la bala se mueve hacia abajo, se gira la imagen 180º, puesto que la imagen de la bala está mirando hacia arriba.

La función collide(), devuelve True si la bala ha salido de la pantalla.

La función update() simplemente actualiza la posición de la bala y la destruye si ha salido de la pantalla.

Vamos a crear en el archivo main.py las balas para comprobar que funciona el código escrito. Pero antes tenemos que controlar los fotogramas por segundos, sino el juego correrá a una velocidad diferente dependiendo de la potencia del ordenador que lo arranque. Escribe en la parte superior del archivo

pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))
# Timing
fps_clock = pygame.time.Clock()
FPS = 60

default_font = pygame.font.Font(None, 28)

En el bucle del juego, escribe lo siguiente.

def main_loop():
    bullets = pygame.sprite.Group()
    running = True
    points = 0
    while running:
        pygame.display.update()
        fps_clock.tick(FPS)
        screen.fill(BLACK)

        if random.randint(1, 5) == 1:
            bullets.add(random_bullet(random.randint(1, 1)))
            points += 1
        draw_text('{} points'.format(points), default_font, screen,
            WIDTH / 2, 20, GREEN)
        bullets.update()
        bullets.draw(screen)

Primero, hemos creado un grupo de sprites llamado bullets para organizar más fácilmente las balas. Hemos creado una variable llamada points que vale 0 al inicio de la partida. Actualizamos la pantalla con pygame.display.update(). Ejecutamos la función tick() para que el juego no pase de los FPS que hemos especificado antes. Después llenamos la pantalla de negro (si no hacemos esto las balas se dibujarán y no se borrarán de su posición antigua). Para que no salgan demasiadas balas de golpe creamos una condición, con if random.randint(1, 5) == 1 hay un 20% de posibilidades de que se dispare una bala y aumente la puntuación en 1 en cada iteración. Fuera de ese if, dibujamos la puntuación actual, actualizamos las balas y las dibujamos en la pantalla.

Para que el código funcione deberás importar el módulo random y todo lo del archivo bullet.py (from bullet import *). Así deberá quedar el código del archivo main.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random

from pygame.locals import *
import pygame

from bullet import *
from global_constants import *

pygame.init()
# Display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
fullscreen = False
# Window titlebar
pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))
# Timing
fps_clock = pygame.time.Clock()
FPS = 60

default_font = pygame.font.Font(None, 28)


def draw_text(text, font, surface, x, y, main_color, background_color=None):
    textobj = font.render(text, True, main_color, background_color)
    textrect = textobj.get_rect()
    textrect.centerx = x
    textrect.centery = y
    surface.blit(textobj, textrect)


def toggle_fullscreen():
    if pygame.display.get_driver() == 'x11':
        pygame.display.toggle_fullscreen()
    else:
        global screen, fullscreen
        screen_copy = screen.copy()
        if fullscreen:
            screen = pygame.display.set_mode((WIDTH, HEIGHT))
        else:
            screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
        fullscreen = not fullscreen
        screen.blit(screen_copy, (0, 0))


def start_screen():
    pygame.mouse.set_cursor(*pygame.cursors.diamond)
    while True:
        title_font = pygame.font.Font('freesansbold.ttf', 65)
        big_font = pygame.font.Font(None, 36)
        default_font = pygame.font.Font(None, 28)
        draw_text('BULLET DODGER', title_font, screen,
                  WIDTH / 2, HEIGHT / 3, RED, YELLOW)
        draw_text('Use the mouse to dodge the bullets', big_font, screen,
                  WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
        draw_text('Press any mouse button or S when you\'re ready',
                  default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
        draw_text('Press F11 to toggle full screen', default_font, screen,
                  WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == pygame.MOUSEBUTTONDOWN:
                main_loop()
                return
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_s:
                    main_loop()
                    return
                if event.key == K_F11:
                    toggle_fullscreen()
            if event.type == QUIT:
                return


def main_loop():
    bullets = pygame.sprite.Group()
    running = True
    points = 0
    while running:
        pygame.display.update()
        fps_clock.tick(FPS)
        screen.fill(BLACK)

        if random.randint(1, 5) == 1:
            bullets.add(random_bullet(random.randint(1, 1)))
            points += 1
        draw_text('{} points'.format(points), default_font, screen,
            WIDTH / 2, 20, GREEN)
        bullets.update()
        bullets.draw(screen)

        for event in pygame.event.get():
            if event.type == QUIT:
                running = False

start_screen()
pygame.quit()

captura-de-pantalla-de-2016-09-11-052228

Paso 5: Crear el personaje

Crea un archivo llamado block.py con el código que aparece a continuación.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *

from global_constants import YELLOW


class Block(pygame.sprite.Sprite):
    def __init__(self):
        super(Block, self).__init__()
        self.img = pygame.Surface((30, 30))
        self.img.fill(YELLOW)
        self.rect = self.img.get_rect()
        self.centerx = self.rect.centerx
        self.centery = self.rect.centery

    def set_pos(self, x, y):
        'Positions the block center in x and y location'
        self.rect.x = x - self.centerx
        self.rect.y = y - self.centery

    def collide(self, sprites):
        for sprite in sprites:
            if pygame.sprite.collide_rect(self, sprite):
                return True

Para crear el personaje no nos hemos complicado mucho: simplemente hemos creado un cuadrado amarillo.

Aparte de importar lo típico en pygame (líneas 3 y 4), importamos de global_constants.py la variable YELLOW para el color del cuadrado. Hemos creado una clase llamada Block, que como Bullet, hereda de pygame.sprite.Sprite. En el constructor, ejecutamos el constructor de la clase padre, creamos una superficie de 30x30 píxeles, la coloreamos de amarillo y obtenemos y guardamos el centro de la coordenadas del cuadrado. Nos interesa el centro, porque cuando creemos movamos el cuadrado con el ratón, el ratón siempre estará en el centro del cuadrado para hacer que el movimiento sea natural (si no, nos moveríamos a partir de la esquina superior izquierda del cuadrado).

El método set_pos, nos permitirá ubicar el cuadrado a partir de las coordenadas que le pasemos como parámetro. El centro del cuadrado estará en las coordenadas x y y.

El método collide devuelve True si el cuadrado colisiona con uno de los sprites del grupo de sprites pasado como parámetro. La colisión se calcula a partir de las coordenadas rectangulares (pygame.sprite.collide_rect(self, sprite)), así que se detecta una colisión cuando se produce una intersección entre los cuadrados de las imagenes sin importar que una parte sea transparente.

En main.py, vamos a importar ahora lo que hemos creado en block.py.

from block import *
from bullet import *
from global_constants import *

En el archivo main.py ya podemos crear el cuadrado y lo ubicarlo inicialmente a partir de las coordenadas del ratón. También vamos a hacer que el ratón sea invisible durante la partida y vamos a crear la variable game_over con el valor False (nos servirá más adelante para detectar el fin del juego).

def main_loop():
    pygame.mouse.set_visible(False)

    square = Block()
    square.set_pos(*pygame.mouse.get_pos())
    bullets = pygame.sprite.Group()
    running = True
    game_over = False

Después comprobamos si ha colisionado con alguna bala, si lo ha hecho hacemos que la variable game_over sea True. Dibujamos el cuadrado con screen.blit(square.img, square.rect) y actualizamos la posición del ratón cada vez que se mueve el ratón dentro del bucle for que recorre los eventos de pygame.

    if square.collide(bullets):
        game_over = True

    screen.blit(square.img, square.rect)
    for event in pygame.event.get():
        if event.type == pygame.MOUSEMOTION:
            mouse_pos = pygame.mouse.get_pos()
            square.set_pos(*mouse_pos)
        if event.type == QUIT:
            running = False

Cuando hayamos terminado, podremos mover el cuadrado amarillo, pero no sabremos si ha colisionado. Si quieres probar si funciona la detección de colisiones antes de seguir, muestra algo por pantalla si se cumple la condición if square.collide(bullets):.

captura-de-pantalla-de-2016-09-11-085128

Paso 6: Crear la pantalla de fin del juego

Dentro del bucle del juego, detrás del bucle for donde detectamos antes los movimientos del ratón, vamos a añadir un bucle que se va a ejecutar mientras la variable game_over sea True.

while game_over:
    pygame.mouse.set_visible(True)
    # Text
    draw_text('{}  points'.format(points), default_font, screen,
              WIDTH / 2, 20, GREEN)
    # Transparent surface
    transp_surf = pygame.Surface((WIDTH, HEIGHT))
    transp_surf.set_alpha(200)
    screen.blit(transp_surf, transp_surf.get_rect())

    draw_text('You lose', pygame.font.Font(None, 40), screen,
              WIDTH / 2, HEIGHT / 3, RED)
    draw_text('To play again press C or any mouse button',
              default_font, screen, WIDTH / 2, HEIGHT / 2.1, GREEN)
    draw_text('To quit the game press Q', default_font, screen,
              WIDTH / 2, HEIGHT / 1.9, GREEN)
    draw_text('Press F11 to toggle full screen', default_font, screen,
              WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)

    pygame.display.update()

    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            if event.key == K_F11:
                toggle_fullscreen()
            if event.key == pygame.K_q:
                game_over = False
                running = False
            elif event.key == pygame.K_c:
                game_over = False
                main_loop()
                return  # Avoids recursion
        if event.type == pygame.MOUSEBUTTONDOWN:
            game_over = False
            main_loop()
            return
        if event.type == QUIT:
            game_over = False
            running = False

Dentro del bucle hemos hecho visible de nuevo el ratón y hemos añadido textos que le indican al usuario lo que puede hacer en este menú. El texto de la puntuación lo hemos puesto tras una superficie transparente (transp_surf). Para que lo que hemos dibujado sea visible ejecutamos el método pygame.display.update().

Después, hemos creado un bucle for para comprobar las acciones del usuario. Si el usuario pulsa F11, se cambiará el juego a modo de pantalla completa; si pulsa la tecla Q o cierra la ventana, la variable game_over será False, por lo que se saldrá de este bucle y del juego; si pulsa la tecla C o pulsa un botón del ratón, el usuario podrá jugar otra partida.

captura-de-pantalla-de-2016-09-11-093528

Paso 7: Aumentar la dificultad a medida que se consiguen más puntos

Escribid lo siguiente en el archivo main.py

points = 0
min_bullet_speed = 1
max_bullet_speed = 1
bullets_per_gust = 1

    while running:
        pygame.display.update()
        fps_clock.tick(FPS)
        screen.fill(BLACK)

        if points >= 2000:
            bullets_per_gust = 3000
            max_bullet_speed = 80
        elif points >= 1000:
            bullets_per_gust = 3
            min_bullet_speed = 3
            max_bullet_speed = 15
        elif points >= 800:
            max_bullet_speed = 20
        elif points >= 600:
            bullets_per_gust = 2
            max_bullet_speed = 10
        elif points >= 500:
            min_bullet_speed = 2
        elif points >= 400:
            max_bullet_speed = 8
        elif points >= 200:
            # The smaller this number is, the probability for a bullet
            # to be shot is higher
            odds = 8
            max_bullet_speed = 5
        elif points >= 100:
            odds = 9
            max_bullet_speed = 4
        elif points >= 60:
            odds = 10
            max_bullet_speed = 3
        elif points >= 30:
            odds = 11
            max_bullet_speed = 2
        elif points < 30:
            odds = 12

        if random.randint(1, odds) == 1:
            for _ in range(0, bullets_per_gust):
                bullets.add(random_bullet(random.randint(min_bullet_speed,
                                                         max_bullet_speed)))
                points += 1
        draw_text('{}  points'.format(points), default_font, screen,
                  WIDTH / 2, 20, GREEN)

Las dificultad la controlamos con el número de balas por ráfaga (bullets_per_gust), la velocidad máxima (max_bullet_speed) y mínima (min_bullet_speed) de las balas y las probabilidades (odds de que se dispare una ráfaga en cada iteración del bucle del juego. Dependiendo de la puntuación, estás variables toman un valor distinto. Cuantos más puntos tienes, más difícil se vuelve el juego.

A continuación os dejo el código definitivo del archivo main.py.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random

import pygame
from pygame.locals import *

from block import *
from bullet import *
from global_constants import *

pygame.init()
# Display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
fullscreen = False
# Window titlebar
pygame.display.set_caption('Bullet dodger')
pygame.display.set_icon(pygame.image.load('bullet.png'))
# Timing
fps_clock = pygame.time.Clock()
FPS = 60

default_font = pygame.font.Font(None, 28)


def draw_text(text, font, surface, x, y, main_color, background_color=None):
    textobj = font.render(text, True, main_color, background_color)
    textrect = textobj.get_rect()
    textrect.centerx = x
    textrect.centery = y
    surface.blit(textobj, textrect)


def toggle_fullscreen():
    if pygame.display.get_driver() == 'x11':
        pygame.display.toggle_fullscreen()
    else:
        global screen, fullscreen
        screen_copy = screen.copy()
        if fullscreen:
            screen = pygame.display.set_mode((WIDTH, HEIGHT))
        else:
            screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
        fullscreen = not fullscreen
        screen.blit(screen_copy, (0, 0))


def start_screen():
    pygame.mouse.set_cursor(*pygame.cursors.diamond)
    while True:
        title_font = pygame.font.Font('freesansbold.ttf', 65)
        big_font = pygame.font.Font(None, 36)
        draw_text('BULLET DODGER', title_font, screen,
                  WIDTH / 2, HEIGHT / 3, RED, YELLOW)
        draw_text('Use the mouse to dodge the bullets', big_font, screen,
                  WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
        draw_text('Press any mouse button or S when you\'re ready',
                  default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
        draw_text('Press F11 to toggle full screen', default_font, screen,
                  WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == pygame.MOUSEBUTTONDOWN:
                main_loop()
                return
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_s:
                    main_loop()
                    return
                if event.key == K_F11:
                    toggle_fullscreen()
            if event.type == QUIT:
                return


def main_loop():
    pygame.mouse.set_visible(False)

    square = Block()
    square.set_pos(*pygame.mouse.get_pos())
    bullets = pygame.sprite.Group()
    running = True
    game_over = False
    points = 0
    min_bullet_speed = 1
    max_bullet_speed = 1
    bullets_per_gust = 1

    while running:
        pygame.display.update()
        fps_clock.tick(FPS)
        screen.fill(BLACK)

        if points >= 2000:
            bullets_per_gust = 3000
            max_bullet_speed = 80
        elif points >= 1000:
            bullets_per_gust = 3
            min_bullet_speed = 3
            max_bullet_speed = 15
        elif points >= 800:
            max_bullet_speed = 20
        elif points >= 600:
            bullets_per_gust = 2
            max_bullet_speed = 10
        elif points >= 500:
            min_bullet_speed = 2
        elif points >= 400:
            max_bullet_speed = 8
        elif points >= 200:
            # The smaller this number is, the probability for a bullet
            # to be shot is higher
            odds = 8
            max_bullet_speed = 5
        elif points >= 100:
            odds = 9
            max_bullet_speed = 4
        elif points >= 60:
            odds = 10
            max_bullet_speed = 3
        elif points >= 30:
            odds = 11
            max_bullet_speed = 2
        elif points < 30:
            odds = 12

        if random.randint(1, odds) == 1:
            for _ in range(0, bullets_per_gust):
                bullets.add(random_bullet(random.randint(min_bullet_speed,
                                                         max_bullet_speed)))
                points += 1
        draw_text('{}  points'.format(points), default_font, screen,
                  WIDTH / 2, 20, GREEN)
        bullets.update()
        bullets.draw(screen)

        if square.collide(bullets):
            game_over = True

        screen.blit(square.img, square.rect)
        for event in pygame.event.get():
            if event.type == pygame.MOUSEMOTION:
                mouse_pos = pygame.mouse.get_pos()
                square.set_pos(*mouse_pos)
            if event.type == QUIT:
                running = False

        while game_over:
            pygame.mouse.set_visible(True)
            # Text
            draw_text('{}  points'.format(points), default_font, screen,
                      WIDTH / 2, 20, GREEN)
            # Transparent surface
            transp_surf = pygame.Surface((WIDTH, HEIGHT))
            transp_surf.set_alpha(200)
            screen.blit(transp_surf, transp_surf.get_rect())

            draw_text('You lose', pygame.font.Font(None, 40), screen,
                      WIDTH / 2, HEIGHT / 3, RED)
            draw_text('To play again press C or any mouse button',
                      default_font, screen, WIDTH / 2, HEIGHT / 2.1, GREEN)
            draw_text('To quit the game press Q', default_font, screen,
                      WIDTH / 2, HEIGHT / 1.9, GREEN)
            draw_text('Press F11 to toggle full screen', default_font, screen,
                      WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)

            pygame.display.update()

            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    if event.key == K_F11:
                        toggle_fullscreen()
                    if event.key == pygame.K_q:
                        game_over = False
                        running = False
                    elif event.key == pygame.K_c:
                        game_over = False
                        main_loop()
                        return  # Avoids recursion
                if event.type == pygame.MOUSEBUTTONDOWN:
                    game_over = False
                    main_loop()
                    return
                if event.type == QUIT:
                    game_over = False
                    running = False

start_screen()
pygame.quit()

Hemos terminado

Si has logrado hacer funcionar el juego, ¡enhorabuena! Si te ha quedado alguna duda, hay algo que no entiendas o crees que he cometido un error, por favor, deja un comentario. El código fuente completo de este juego lo he incluido en un repositorio llamado pygame_stuff. El repositorio también contiene otros juegos y plantillas de código para facilitar la tarea de programar usando pygame. Con la dirección https://notabug.org/jorgesumle/pygame_stuff puedes acceder al código y clonar el repositorio (git clone https://notabug.org/jorgesumle/pygame_stuff).

Comentarios