Programar Juegos Arcade con Python y Pygame

Programar Juegos Arcade
con Python y Pygame

Chapter 13: Introducción a los Sprites

Vídeo: Introducción a los Sprites

Todos nuestros juegos necesitan de algo que pueda gestionar objetos que se tocan. Pelotas que salen rebotadas de una raqueta, extraterrestres alcanzados por un rayo láser, o nuestro personaje favorito recogiendo una moneda. Todos ellos requieren lo que se denomina detección de colisiones.

La biblioteca Pygame posee soporte para sprites. Un sprite es una imagen bidimensional que forma parte de una escena gráfica aun mayor. Habitualmente, un sprite es cierto tipo de objeto que interactúa. Por ejemplo, un coche, una rana o nuestro pequeño fontanero.

fig.sprite

Originalmente, las videoconsolas venían equipadas con hardware para recrear los sprites. En la actualidad este hardware especializado ya no es necesario, pero seguimos usando el término “sprite.”

13.1 Sprites Básicos y Colisiones

Veremos paso a paso un programa que utiliza sprites. En este ejemplo se muestra cómo crear una pantalla con bloques negros, que recogeremos mediante un bloque rojo controlado por el ratón, tal como se puede ver en la Figura 13.1. El programa mantiene un “marcador” con los bloques que llevemos recogidos. El código para este ejemplo lo podemos encontrar en:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=sprite_collect_blocks.py

fig.example_sprite_game1
Figure 13.1: Ejemplo de un Juego con Sprites

La primera línea de nuestro programa empieza como otros tantos juegos que ya hemos hecho:

import pygame
import random

# Definimos algunos colores
NEGRO  = (  0,   0,   0)
BLANCO = (255, 255, 255)
ROJO   = (255,   0,   0)

Para tener soporte de sprites, importamos la biblioteca Pygame en la línea 1. Para colocar aleatoriamente los bloques, importamos la biblioteca random en la línea 2. La definición estándar de los colores la encontramos entre las líneas 5-7; hasta aquí no hay nada que no hayamos visto ya.

class Bloque(pygame.sprite.Sprite):
    """
    Esta clase representa la pelota.
    Deriva de la clase "Sprite" en Pygame.
    """

En la línea 9 comienza la definición de la clase Bloque. Observa que esta es un clase hija de la clase Sprite. El texto pygame.sprite. especifica la biblioteca y el paquete, algo que trataremos en el capítulo 14. Todas las funcionalidades por defecto de la clase Sprite serán ahora parte de la clase Bloque.

    def __init__(self, color, largo, alto):
        """ Constructor. Pasa el color del bloque,
        así como su posición x e y. """

        # Llama al constructor de la clase padre (Sprite)
        super().__init()__

En la línea 15, el constructor de la clase Bloque recibe un parámetro para self, tal como sucede para cualquier otro constructor. También toma parámetros que definen el color, el alto y el largo del objeto.

Es importante llamar al constructor de la clase padre en Sprite para permitir que los sprites se inicialicen. Esto se consigue en la línea 20.

        # Crea una imagen del bloque y lo rellena de color.
        # También podría tratarse de una imagen cargada desde el disco.
        self.image = pygame.Surface([largo, alto])
        self.image.fill(color)

Las líneas 24 y 25 crean la imagen que finalmente aparecerá en la pantalla. La línea 24 crea una imagen en blanco. La línea 25 la rellena de negro. Si el programa necesita dibujar una figura distinta al bloque negro, deberemos modificar las siguientes líneas de código:

Por ejemplo:

    def __init__(self, color, largo, alto):
        """
        Constructor Elipse. Pasa el color de la elipse,
        y sus dimensiones.
        """
        # Llama al constructor de la clase padre (Sprite)
        super().__init__()

        # Establece el color de fondo y que éste será transparente.
        self.image = pygame.Surface([largo, alto])
        self.image.fill(BLANCO)
        self.image.set_colorkey(BLANCO)

        # Dibuja la elipse
        pygame.draw.ellipse(self.image, color, [0, 0, largo, alto])

Si emplearamos el código anterior, todo tendría forma de elipse. La línea 29 dibuja la elipse y la 26 hace que el blanco sea transparente de forma que resalte el color de fondo. Es el mismo concepto que ya vimos en el capítulo 11, donde convertíamos en transparente el color blanco del fondo de la imagen.

    def __init__(self):
        """ Constructor Gráfico del Sprite . """

        # Llama al constructor de la clase padre (Sprite)
        super().__init__()

        # Carga la imagen
        self.image = pygame.image.load("jugador.png").convert()

        # Establece el color transparente
        self.image.set_colorkey(BLANCO)

En el código anterior cambiamos el origen del sprite. Si lo que deseamos es un mapa de bits, cargamos una imagen (línea 22) y determinamos que el fondo transparente sea de color blanco (línea 25). En este caso, las dimensiones del sprite se ajustarán automáticamente a las del gráfico, con lo que no necesitamos pasárselas a la función. Observa como en la línea 15 ya no aparecen esos parámetros.

Independientemente del tipo de sprite que tengamos, aún existe otra línea que necesitamos para nuestro constructor:

        # Captura el objeto rectángulo que posee las dimensiones de la imagen.
        # Actualiza la posición de ese objeto estableciendo los valores de
        # rect.x y rect.y
        self.rect = self.image.get_rect()

El atributo rect, es una variable que, a su vez, es instancia de la clase Rect que proporciona Pygame. Este rectángulo representa las dimensiones del sprite. La clase rectángulo tiene unos atributos para x e y que hay que establecer. Pygame dibujará el sprite donde se encuentren esos atributos x e y. Por eso, para mover el sprite, el programador necesita establecer mySpriteRef.rect.x y mySpriteRef.rect.y, donde mySpriteRef es la variable que apunta al sprite.

Bueno, ya hemos terminado con la clase Bloque. Es tiempo de empezar con el código de iniciación.

# Inicializamos Pygame
pygame.init()

# Establecemos el largo y alto de la pantalla
largo_pantalla = 700
alto_pantalla = 400
pantalla = pygame.display.set_mode([largo_pantalla, alto_pantalla])

El código anterior inicializa Pygame y crea una ventana para el juego. Hasta aquí nada nuevo respecto a otros programas Pygame.

# Esta es una lista de 'sprites.' Cada bloque del programa es
# añadido a esta lista.
# La lista es gestionada por una clase llamada 'Group.'
bloque_lista = pygame.sprite.Group()

# Esta es una lista de cada sprite.
# Incluido el del protagonista también.
listade_todoslos_sprites = pygame.sprite.Group()

Una ventaja importante de trabajar con sprites, es la posibilidad de agruparlos. Con un solo comando podemos dibujar, y mover a la vez, todos los sprites que se encuentren agrupados. Podemos también registrar colisiones de sprites contra un grupo entero.

El código anterior crea dos listas. La variable listade_todoslos_sprites contendrá cada sprite del juego. Usaremos esta lista para dibujar todos los sprites. La variable bloque_lista contiene cada objeto con los que puede colisionar el protagonista. En este ejemplo se incluirá cada objeto del juego, excepto al protagonista. No queremos que el protagonista se encuentre en esta lista. Si estuviera en ella, Pygame siempre nos devolvería que el protagonista está chocando, y no podríamos comprobar cuándo es que realmente colisiona con los objetos del bloque_lista.

for i in range(50):
    # Esto representa un bloque
    bloque = Bloque(NEGRO, 20, 15)

    # Establece una ubicación aleatoria para el bloque
    bloque.rect.x = random.randrange(largo_pantalla)
    bloque.rect.y = random.randrange(alto_pantalla)

    # Añade el bloque a la lista de objetos
    bloque_lista.add(bloque)
    listade_todoslos_sprites.add(bloque)

El bucle que comienza en la línea 49 añade 50 bloques sprite negros a la pantalla. La línea 51 crea un bloque nuevo, establece su color, su largo y su altura. Las líneas 54 y 55 establecen las coordenadas donde aparecerá el objeto. La línea 58 añade el bloque a la lista de bloques con los que el protagonista puede colisionar. La línea 59 lo añade a la lista de todos los bloques. Esto debería parecerse bastante al código que escribiste para la práctica del Taller 13.

# Creamos un bloque protagonista ROJO
protagonista = Bloque(ROJO, 20, 15)
listade_todoslos_sprites.add(protagonista)

Las líneas 61-63 determinan al protagonista de nuestro juego. La línea 62 crea un bloque rojo que funcionará finalmente como el protagonista. Para que este bloque se pueda dibujar, en la línea 63 lo añadimos a listade_todoslos_sprites, y no en bloque_lista.

# Iteramos hasta que el usuario pulse el botón de cerrar.
hecho = False

# Determinamos cuán rápido se refresca la pantalla
reloj = pygame.time.Clock()

marcador = 0

# -------- Bucle Principal del Programa -----------
while not hecho:
    for evento in pygame.event.get():
        if evento.type == pygame.QUIT:
            hecho = True

    # Limpiamos la pantalla
    pantalla.fill(BLANCO)

El anterior código es el estándar que ya introdujimos en el Capítulo 5. La línea 71 inicializa nuestra variable marcador a 0.

    # Obtenemos la posición actual del ratón como
    # una lista de dos números.
    pos = pygame.mouse.get_pos()

    # Extraemos los valores x e y de la lista,
    # de la misma forma que extraemos letras de una cadena de caraceteres (string).
    # Colocamos al objeto protagonista en la ubicación del ratón
    protagonista.rect.x = pos[0]
    protagonista.rect.y = pos[1]

Tal como ya vimos en otros programas Pygame, la posición del ratón es recogida por la línea 84. Lo nuevo e importante está contenido entre las líneas 89-90, donde el rectángulo que contiene al sprite es trasladado a una nueva ubicación. Recordemos que este rectángulo fue creado previamente en la línea 31, y que sin esta parte, el código no funcionaría.

    # Observamos si el bloque protagonista ha colisionado con algo.
    lista_impactos_bloques = pygame.sprite.spritecollide(protagonista, bloque_lista, True)

Esta línea de código toma al sprite referenciado como protagonista y lo chequea frente al resto de sprites en bloque_lista. El código devuelve una lista con los sprites que coincidan con él. Si no hubieran sprites coincidentes, devolvería una lista vacía. El operador booleano True, retirará de la lista a todos los sprites que coincidan. Si este operador estuviera establecido como False, los sprites no serían retirados.

    # Comprobamos la lista de colisiones.
    for bloque in lista_impactos_bloques:
        marcador += 1
        print(marcador)

Este bucle itera sobre cada uno de los sprites de la lista de colisiones creada anteriormente en la línea 93. Si hubieran sprites en esa lista, incrementaría el marcador por cada una de las colisiones. Luego mostraría el marcador en pantalla. Observemos que el comando print de la línea 98, no imprimirá el marcador en la ventana principal junto a los sprites, sino en la consola. ¿Cómo conseguir que el marcador aparezca sobre la ventana principal? Eso es parte del Taller 14.

    # Dibujamos todos los sprites
    listade_todoslos_sprites.draw(pantalla)

La variable listade_todoslos_sprites, que pertenece a la clase Group, posee un método llamado draw. Este método itera a través de la lista llamando al método draw en cada uno de los sprites. Esto significa, que con solo una línea de código, el programa consigue que cada uno de los sprites en listade_todoslos_sprites sea dibujado.

    # Limitación de 60 fotogramas por segundo
    reloj.tick(60)

    # Avanzamos y actualizamos la pantalla con todo aquello que ha sido dibujado.
    pygame.display.flip()

pygame.quit()

Las líneas 103-109 refrescan la pantalla, y llaman al método quit cuándo el bucle principal termina.

13.2 Sprites En Movimiento

Vídeo: Sprites En Movimiento

Hasta ahora, en el ejemplo que hemos tratado, solo el sprite protagonista se mueve. ¿Cómo podemos conseguir que el programa mueva todos los sprites? Pues muy fácil, sólo necesitamos dos pasos adicionales:

Lo primero es añadir un método nuevo a la clase Bloque. Llamaremos update a este nuevo método. La función update será llamada automáticamente cuando update sea llamado para la lista entera.

Colocamos esto en el sprite:

    def update(self):
        """ Llamado en cada fotograma. """

        # Desplazamos el bloque hacia abajo un píxel
        self.rect.y += 1

Colocamos esto dentro del bucle principal:

    # Llamamos al método update() para todos los bloques en bloque_lista
    bloque_lista.update()

El código dista todavía de ser perfecto. De momento, todos los bloques escapan de la pantalla sin volver a aparecer. Las siguientes líneas mejorarán la función update de forma que los bloques vuelvan a aparecer por la parte superior de la pantalla.

    def update(self):
        # Desplaza el bloque hacia abajo un píxel
        self.rect.y += 1
        if self.rect.y > alto_pantalla:
			self.rect.y = random.randrange(-100, -10)
			self.rect.x = random.randrange(0, largo_pantalla)

Si lo que queremos es que el programa resetee las posiciones de los bloques en la parte superior de la pantalla, escribiremos las siguientes líneas:

    def reset_pos(self):
        """ Reseteamos la posición en la parte superior de la pantalla en ubicaciones aleatorias de x.
	    Llamada por update() o por el bucle principal si ocurre una colisión.
        """
        self.rect.y = random.randrange(-300, -20)
        self.rect.x = random.randrange(0, largo_pantalla)

    def update(self):
        """ Llamado en cada fotograma. """

        # Desplazamos un píxel hacia abajo el bloque  
        self.rect.y += 1

        # Si el bloque está muy abajo, lo devuelve a la parte superior de la pantalla.
        if self.rect.y > 410:
            self.reset_pos()

En lugar de que los bloques se destruyan al colisionar, podríamos hacer que el programa llamara a la función reset_pos y que el bloque se desplazara a la parte superior de la pantalla, listo para ser recogido.

    # Observa si el bloque protagonista ha colisionado con algo.
    lista_impactos_bloques = pygame.sprite.spritecollide(protagonista, bloque_lista, False)

    # Comprueba la lista de colisiones.
    for bloque in lista_impactos_bloques:
        marcador += 1
        print(marcador)

        # Resetea el bloque hacia la parte superior para que vuelva a caer.
        bloque.reset_pos()

El código completo está aquí:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=moving_sprites.py

Si en lugar de eso quieres el código para sprites que rebotan, mira aquí:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=moving_sprites_bounce.py

Si lo que quieres es que se muevan en círculos:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=sprite_circle_movement.py

13.3 La Clase Juego

Anteriormente, en el Capítulo 9, introdujimos las funciones. Al final de ese capítulo hablamos de una opción que era la función main. A medida que los programas se hacen extensos, esta técnica nos ayudará a evitar futuros problemas que puedan provenir de tener que recorrer un montón de código. De momento, nuestros programas no son muy largos. Sin embargo, conozco algunas personas a las cuales les gusta tener todo organizado desde el principio.

Para esas personas que ya se enfrentan a códigos muy largos, esta es una técnica opcional para mantener organizado el código. (Si no perteneces a este grupo todavía, puedes saltarte esta sección y marcarla para cuando tus programas se vuelvan extensos.) Mira el vídeo para que tengas una idea de como trabaja el programa.
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=game_class_example.py

13.4 Otros Ejemplos

Aquí debajo encontrarás más ejemplos de lo que puedes hacer con sprites. Algunos, incluso, incluyen un vídeo que explica como funcionan.

13.4.1 ¿Disparamos?

fig.bullets
Figure 13.2: Disparamos?

¿Estás interesado en un juego donde haya que pegar tiros? Algo así como el clásico Space Invaders? En el siguiente ejemplo se explica cómo crear sprites que representen dianas:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=bullets.py

13.4.2 Paredes

¿A lo mejor lo que buscas más bien, es un juego del tipo aventuras? ¿No quieres que tu protagonista vague libremente por todo el espacio? El ejemplo siguiente te muestra cómo agregar paredes que entorpezcan los movimientos del protagonista:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=move_with_walls_example.py

fig.walls
Figure 13.3: Podemos tropezarnos con muros

¡Espera! ¿No tienes suficiente para tu aventura con una sola habitación? ¿Quieres que tu personaje se desplace entre distintas pantallas? ¡Pues lo podemos hacer! Mira el siguiente ejemplo donde el personaje se tiene que desplazar entre un laberinto de estancias:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=maze_runner.py

fig.maze_runner
Figure 13.4: Laberinto

13.4.3 Plataformas

¿Te interesa un juego de plataformas como Donkey Kong? Podemos usar el mismo concepto que hemos empleado para las paredes, pero añadiéndole un toque de fuerza de gravedad:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=platform_jumper.py

fig.platform_jumper
Figure 13.5: Saltando entre plataformas

Las buenas plataformas se mueven de un lado a otro. Este sería un ejemplo:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=platform_scroller.py

fig.platform_scroller
Figure 13.6: Juego de plataformas de desplazamiento lateral

Los juegos de plataformas más divertidos tienen superficies que se mueven. Observa cómo lo hemos hecho en el ejemplo siguiente:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=platform_moving.py

fig.platform_moving
Figure 13.7: Plataformas móviles

13.4.4 Serpiente/Ciempiés

De vez en cuando me he cruzado con alumnos que quieren construir un juego del tipo “serpiente” o “ciempiés”. Aquí te encuentras con una serpiente que posee varios segmentos que debes controlar. Esto requiere que cada segmento se guarde en una lista. Aunque aprender a hacer esto requiere de dos nuevos comandos, el concepto que hay detrás de ello no es muy difícil de comprender.

Controlar una serpiente o ciempiés que se mueven alrededor de la pantalla:
ProgramArcadeGames.com/python_examples/f.php?lang=es&file=snake.py

fig.snake
Figure 13.8: Serpiente

13.4.5 Usando Hojas de Sprites

Este es un ejemplo bastante extenso sobre el uso de “hojas de sprites”, las cuales proporcionarán los gráficos de fondo necesarios, para un juego de plataformas. También permite múltiples niveles y plataformas móviles. El juego se ha dividido en varios archivos. ProgramArcadeGames.com/python_examples/sprite_sheets

fig.sprite_sheet
Figure 13.9: Plataformas con hojas de sprites

13.4.6 Test

Haz click para ir al Test.

13.4.7 Ejercicios

No hay ejercicios para este capítulo.

13.4.8 Taller

Haz click para ir al Taller.


You are not logged in. Log in here and track your progress.