1. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
Monkey Hunter: Plataformas
(Código de Hugo Ruscitti, http://www.losersjuegos.com.ar ligeramente modificado)
Introducción
Monkey Hunter es un (casi) juego de tipo plataforma que se desarrolló como muestra/
tutorial en las Conferencias Abiertas de Software Libre y GNU/Linux CaFeConf. El video
de la charla (41m59s), explicando el proceso, lo puedes encontrar en
http://video.google.es/videoplay?docid=-4248728848273927944#
y la presentación de la charla en
http://www.cafeconf.org/2007/slides/hugo_ruscitti_pygame.pdf
A efectos pedagógicos hemos modificado ligeramente algunos nombres y hemos eliminado
las lineas que indican la licencia en cada archivo. No obstante, el código original puedes
obtenerlo en:
http://www.cafeconf.org/2007/slides/hugo_ruscitti_ejemplos_pygame.tar.gz
Hablando de la licencia, cuando reúses código de terceras personas siempre te encontrarás
algo similar. Es lo habitual. Y debes respetar sus condiciones de uso. La del programa que
nos ocupa es la GPL2, como puede verse en la parte que hemos omitido:
PÁGINA 1 DE 27
CC: FERNANDO SALAMERO
2. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
# -*- coding: utf-8 -*-
#
# Copyright 2007 Hugo Ruscitti <hugoruscitti@gmail.com>
# More info: http://www.losersjuegos.com.ar
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
# USA
El proyecto es un buen ejemplo, no sólo de como programar un juego de este tipo, si no
también de cómo organizar el código en diferentes archivos para que sea más fácil
su mantenimiento. Piensa que cuando un proyecto crece lo suficiente, el número de lineas
de código puede ser brutal. Afortunadamente, desde cualquier archivo, Python permite
cargar cualquier otro como si fuera un módulo usando la instrucción import.
A continuación, la descripción de los diferentes archivos que componen el proyecto:
1. monkeyhunter.py
El programa principal y el que hay que ejecutar para lanzar el juego.
2. util.py
Contiene funciones útiles para realizar diferentes tareas.
3. sonidos.py
Define los diferentes sonidos del juego y cómo ejecutarlos.
4. mono.py
Contiene la clase Mono, derivada de la clase pygame.sprite.Sprite.
5. cazador.py
Se trata de la clase Cazador (los enemigos), otro sprite.
6. bomba.py
clase Bomba que representa los sprites de las bombas.
7. banana.py
Las bananas son objetos de la clase Banana, implementados en este archivo.
8. escenario.py
La clase Escenario controla el laberinto del juego No es un sprite.
PÁGINA 2 DE 27
CC: FERNANDO SALAMERO
3. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
9. explosion.py
La clase Explosion, otro sprite, debe aparecer temporalmente cuando se estalla una
bomba.
10. Carpetas ‘images’ y ‘sounds’
Las imágenes y los sonidos del juego se encuentran es estas carpetas.
Bien, pasemos a los detalles del código:
monkeyhunter.py
# -*- coding: utf-8 -*-
import pygame
import util
import sonidos
from mono import Mono
from explosion import Explosion
from escenario import Escenario
pygame.init()
visor = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Monkey Hunter")
fondo = util.cargar_imagen('escenario.jpg', optimizar=True)
logotipo = util.cargar_imagen('logo.png')
decoracion = util.cargar_imagen('decoracion.png')
sprites = pygame.sprite.OrderedUpdates()
bananas = pygame.sprite.Group()
bombas = pygame.sprite.Group()
cazadores = pygame.sprite.Group()
escenario = Escenario()
escenario.imprimir(fondo)
escenario.crear_objetos(bananas, bombas, cazadores)
mono = Mono(escenario)
sprites.add(bananas)
sprites.add(bombas)
sprites.add(cazadores)
sprites.add(mono)
salir = False
reloj = pygame.time.Clock()
PÁGINA 3 DE 27
CC: FERNANDO SALAMERO
4. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
while not salir:
reloj.tick(60)
for evento in pygame.event.get():
if evento.type == pygame.QUIT:
salir = True
elif evento.type == pygame.KEYDOWN:
if evento.unicode == 'q':
salir = True
elif evento.unicode == 'f':
pygame.display.toggle_fullscreen()
banana_en_colision = util.spritecollideany(mono, bananas)
if banana_en_colision and banana_en_colision.se_puede_comer:
banana_en_colision.comer()
mono.ponerse_contento()
bomba_en_colision = util.spritecollideany(mono, bombas)
if bomba_en_colision and not bomba_en_colision.esta_cerrada:
sprites.add(Explosion(bomba_en_colision))
bomba_en_colision.kill()
sonidos.reproducir_sonido('pierde')
sonidos.reproducir_sonido('boom')
mono.pierde_una_vida()
for c in cazadores:
c.ponerse_contento()
cazador_en_colision = util.spritecollideany(mono, cazadores)
if cazador_en_colision:
cazador_en_colision.kill()
sonidos.reproducir_sonido('pierde')
mono.pierde_una_vida()
sprites.update()
visor.blit(fondo, (0, 0))
sprites.draw(visor)
visor.blit(decoracion, (0, 0))
visor.blit(logotipo, (640 - 67, 480 - 85))
pygame.display.update()
Empezamos el juego, tras la declaración de la codificación unicode, con la importación
del módulo pygame. Así mismo, importamos nuestro módulo util (es decir, el archivo de
código util.py) que incluye funciones útiles para el desarrollo del programa y lo mismo
sucede con el módulo sonidos (archivo sonidos.py).
PÁGINA 4 DE 27
CC: FERNANDO SALAMERO
5. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
La siguiente importación
from mono import Mono
importa la clase Mono desde nuestro módulo mono (archivo mono.py). ¿Por qué
hacerlo así y no, simplemente, escribir import mono? El uso de from en las
instrucciones de importación permite que llamemos a los objetos importados
directamente por su nombre, sin necesidad de escribir delante el nombre del módulo.
De esta forma, para referirnos a la clase Mono, bastará que escribamos Mono. Si lo
hubiéramos hecho de la otra forma, tendríamos que escribir siempre mono.Mono para
indicar que la clase Mono pertenece al módulo mono. Generalmente, esto se realiza
cuando no hay problemas de colisión de nombres; imagínate que en dos módulos
distintos tenemos dos funciones que poseen el mismo nombre... Allí sería importante
referirnos a ellas con el nombre completo consistente en
nombreModulo.nombreObjeto. En cualquier caso, de la misma forma, se importan
las clases Explosion y Escenario de sus respectivos módulos en las siguientes líneas.
A continuación, tras la habitual inicialización del entorno de PyGame con init() y la
definición de la pantalla del juego (visor, inicialmente una ventana de 640x480 pixeles
con el título ‘Monkey Hunter’), cargamos las imágenes estáticas del juego: fondo (la
imagen de fondo escenario.jpg), logotipo (el pirata, logotipo del juego, logo.png) y
decoracion (la decoración de ramas de los bordes del juego, decoracion.png). En todos
los casos se usa la función cargar_imagen() del módulo util.
Las siguientes líneas configuran los sprites del juego. De hecho, el Grupo que los incluye a
todos, lo denominamos sprites. Fíjate que para su creación se usa
sprites = pygame.sprite.OrderedUpdates()
en lugar del que hemos venido usando, RenderUpdates(). OrderedUpdates es una
clase derivada que tiene la virtud de redibujar, en su momento, todos los sprites en el
orden en el que fueron creados (de ahí su nombre). De esta manera, después de crear
los otros grupos de sprites
bananas = pygame.sprite.Group()
bombas = pygame.sprite.Group()
cazadores = pygame.sprite.Group()
y tras añadirlos a sprites
sprites.add(bananas)
sprites.add(bombas)
sprites.add(cazadores)
sabemos que los sprites del grupo cazadores siempre se dibujarán por encima de los
sprites del grupo bombas que a su vez se dibujarán por encima de los sprites del
grupo bananas.
Lo siguiente es crear escenario, un objeto de tipo Escenario que contiene el laberinto
del juego. Observa, de hecho, que lo primero que se hace es
escenario.imprimir(fondo)
PÁGINA 5 DE 27
CC: FERNANDO SALAMERO
6. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
que, como veremos, dibuja en la surface fondo el laberinto generado. Finalmente, se le
añaden los objetos que se crearán al azar, pasándole los grupos de sprites que van a poblar
el laberinto:
escenario.crear_objetos(bananas, bombas, cazadores)
Cuando programes un juego, esta manera de trabajar es muy típica. Primero escribo el
esqueleto del juego, con nombres de funciones, objetos y clases que se encargarán de
realizar diferentes tareas. Y más tarde, implemento su código en los módulos
correspondientes. Dividir un tarea grande en una serie de tareas más pequeñas
es una estrategia muy ventajosa.
Falta por añadir el sprite mono, el protagonista de nuestro juego. Fíjate en el detalle de
que en la creación del objeto
mono = Mono(escenario)
se le pasa como argumento el objeto escenario (lo veremos en su implementación, en el
módulo mono.py). Además, como se añade en último lugar al grupo sprites, nuestro
mono siempre se dibujará por encima de todos los demás.
Bien. Sólo queda definir la variable boolena de estado salir, que controlará cuándo se
termina el juego y crear el objeto reloj para forzar, como hemos visto repetidamente, que
la animación se realice a los frames por segundo deseados.
En el bucle del juego, establecemos en 60 la cantidad de fps que acabamos de citar y
atacamos la cola de eventos. Allí implementamos el que, o bien cerrando la ventana o
bien pulsando la tecla ‘q’, el juego finalice. Observa que escribimos pygame.QUIT y no
simplemente QUIT ya que, en la sección de importaciones de módulos, no hemos puesto
el habitual from pygame.locals import *. Por esa misma causa, no usamos para
identificar las teclas evento.key por no poner cosas similares a pygame.K_q o
pygame.K_f. Usamos en su lugar el ya conocido evento.unicode.
Otra cosa notable; si se pulsa la tecla ‘f’ el juego pasa a pantalla completa si no lo estaba y
viceversa. Esto se consigue con la línea
pygame.display.toggle_fullscreen()
ya que toggle_fullscreen() actúa como un conmutador entre ambos estados.
Es el turno de las colisiones. Lo primero es ver si el jugador alcanza una banana. La
técnica es muy clara:
banana_en_colision = util.spritecollideany(mono, bananas)
La función spritecollideany() es una función que hemos definido en el módulo util (es
decir, el archivo util.py). Hemos mantenido el nombre de otra función similar de
PyGame, pygame.sprite.spritecollideany(); el problema con ésta es que sólo devuelve
True o False para indicar si se ha producido colisión. Nosotros necesitamos que la
función devuelva el sprite con el que se ha colisionado (en el apartado del módulo
util veremos cómo la hemos definido). En definitiva, la línea anterior, mira el sprite del
grupo bananas con el que ha colisionado el sprite mono y lo almacena en
banana_en_colision.
PÁGINA 6 DE 27
CC: FERNANDO SALAMERO
7. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
¿Qué hacemos con ello? No hay que olvidar que banana_en_colision ahora es un sprite
del grupo bananas (y luego veremos cómo es su definición). La cuestión es que se usa su
atributo se_puede_comer para comprobar que es una banana comestible y en tal caso
se llama a dos métodos; el método comer() de los sprites bananas, para que sea comida
(y desaparezca), y el método ponerse_contento() del sprite mono para que éste
reaccione.
Algo parecido se hace en
bomba_en_colision = util.spritecollideany(mono, bombas)
esta vez mirando si hemos chocado con una bomba. Aquí hay que hacer más cosas,
siempre que la bomba esté activa (por eso se mira en un if usando el atributo, de un sprite
bombas, denominado esta_cerrada):
1. Crear el sprite de la explosión para mostrar la correspondiente animación, es decir,
un objeto de la clase Explosion.
2. Eliminar el sprite de la bomba explotada con kill().
3. Reproducir los sonidos pertinentes con la función reproducir_sonido() del
módulo sonidos (archivo sonidos.py).
4. Quitar una vida al mono con su método pierde_una_vida(). Esta función es un
lugar típico donde escribir código para reajustar al jugador (por ejemplo, que vuelva
a empezar en la posición inicial, etc)
5. Hacer reaccionar a los enemigos, recorriendo todos los sprites del grupo cazadores
e invocando su método ponerse_contento().
Ya hemos gestionado las bombas, vamos a ir ahora al caso de que el jugador colisione con
uno de los cazadores... Una vez que se ha determinado con qué cazador se ha chocado,
cazador_en_colision (fíjate en el if: recuerda que si una variable tiene un valor no nulo,
en un if es interpretada como True; si no hay colisión cazador_en_colision toma el
valor None que se interpreta en un if como False), realizamos las tareas lógicas. Primero
eliminamos al cazador con kill(), luego reproducimos el sonido correspondiente y
finalmente llamamos al método pierde_una_vida().
El bloque siguiente, una vez determinado el estado del juego, es el dibujado en pantalla.
Como lo tenemos todo organizado correctamente en sprites, esto es muy sencillo de hacer;
basta seguir el esquema habitual:
sprites.update()
visor.blit(fondo, (0, 0))
sprites.draw(visor)
visor.blit(decoracion, (0, 0))
visor.blit(logotipo, (640 - 67, 480 - 85))
pygame.display.update()
En efecto:
1. Se computan las nuevas posiciones de los sprites con su método update().
2. Se redibuja el fondo completo (borrando por completo el fotograma anterior) con
blit().
3. Se dibujan los sprites con draw().
PÁGINA 7 DE 27
CC: FERNANDO SALAMERO
8. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
4. Dibujamos encima la decoración, de nuevo con blit().
5. Hacemos lo propio con el logotipo (que tiene un tamaño de 67x85 pixeles y queda
exactamente en la esquina inferior derecha)
6. Volcamos todo el resultado en pantalla con pygame.dislplay.update()
¡Ya está! El resto es implementar cada uno de los tipos de sprites y los módulos auxiliares
en los diferentes archivos. Vamos a ello.
util.py
import os
import pygame
from random import randint
def cargar_imagen(nombre, optimizar=False):
ruta = os.path.join('images', nombre)
imagen = pygame.image.load(ruta)
if optimizar:
return imagen.convert()
else:
return imagen.convert_alpha()
def cargar_sonido(nombre):
ruta = os.path.join('sounds', nombre)
return pygame.mixer.Sound(ruta)
def spritecollideany(sprite, grupo):
funcion_colision = sprite.rect_colision.colliderect
for s in grupo:
if funcion_colision(s.rect_colision):
return s
return None
def a_coordenadas(fila, columna):
return (60 + columna * 48, 80 + fila * 43)
def a_celdas(pos_x, pos_y):
return ((pos_y - 80) / 43, (pos_x - 60) / 48)
En el módulo util definimos una serie de funciones que se emplean en el resto del juego.
Éstas son:
PÁGINA 8 DE 27
CC: FERNANDO SALAMERO
9. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
1. cargar_imagen()
Carga un archivo de imagen y lo incorpora a PyGame. La función toma dos
argumentos. El primero, nombre, es el nombre del archivo que contiene la imagen y
el segundo, optimizar (que si no se indica lo contrario vale False), indica si la
imagen tiene ya de por si transparencia o no.
El código nos tendría que sonar. Primero se usa os.path.join() (fíjate que se ha
importado previamente el módulo os) para indicar que todas nuestras imágenes
estarán en la subcarpeta images. Luego se carga la imagen que se ha pasado como
argumento y, según sea de un tipo u otro, se convierte adecuadamente y se devuelve
como resultado de la función con return.
2. cargar_sonido()
Hace algo similar que la función anterior, pero esta vez con sonidos. El sonido se
devuelve convertido con return pygame.mixer.Sound(ruta).
3. spritecollideany()
La hemos nombrado antes. Toma dos argumentos; el primero es un sprite, sprite, y el
segundo un grupo, grupo. El objetivo de la función es devolver el primer sprite de
grupo que colisiona con sprite. Para ello, necesitamos una función que se encargue
de detectar cuando dos sprites colisionan. PyGame incorpora unas cuantas (hay
muchas maneras distintas de hacerlo; consulta la documentación). En cualquier caso,
como veremos más tarde, los sprites que usamos tendrán un atributo llamado
rect_colision de tipo Rect que indicará la zona que se usará para considerar el
choque. Los objetos de tipo Rect, por otra parte, poseen un método denominado
colliderect() que determina si ha colisionado con otro Rect. Así, la línea
funcion_colision = sprite.rect_colision.colliderect
lo que hace es almacenar en funcion_colision dicha función. Ella es, pues, la
función que se va a usar para ver si el sprite ha colisionado o no con algún otro. El
resto debería ser fácil de entender; recorremos con un bucle for todos los sprites del
grupo y, si hay una colisión, con return s se devuelve el sprite del grupo culpable.
La última línea
return None
indica claramente que se devolverá None en el caso de que no tengamos ninguna
colisión.
4. a_coordenadas() y a_celdas()
Si has probado el juego, verás que el mono no se mueve de forma fluida, si no que
avanza paso a paso. En lugar de cambiar su posición pixel a pixel, moveremos los
sprites como si se desplazaran por un cuadrícula invisible (al estilo del juego de los
barcos), de celda en celda. Por ello vamos a necesitar un par de funciones que dadas
las coordenadas (x, y) en pantalla no dé la posición en forma de (fila, columna) en
la cuadrícula y viceversa. De lo primero se encarga la función a_celdas() y de lo
segundo a_coordenadas(). Intenta entender las cuentas. Hazte una cuadrícula en
un papel y trata de comprender las operaciones matemáticas que se hacen para pasar
de lo uno a lo otro.
PÁGINA 9 DE 27
CC: FERNANDO SALAMERO
10. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
sonidos.py
import pygame
import util
pygame.mixer.pre_init(44100,16,2,1024)
pygame.mixer.init()
come_fruta = util.cargar_sonido('come_fruta.wav')
pierde_vida = util.cargar_sonido('pierde_vida.wav')
boom = util.cargar_sonido('explosion.wav')
def reproducir_sonido(nombre):
sonidos = {
'come': come_fruta,
'pierde': pierde_vida,
'boom': boom,
}
sonidos[nombre].play()
El módulo sonidos es breve y bastante sencillo. Lo primero que realiza es inicializar por
separado el módulo mixer que se encarga de gestionar los sonidos y la música en PyGame.
Observa que para que los sonidos no nos salgan con retardo en la animación usamos algo
ya nos hemos encontrado en otros programas:
pygame.mixer.pre_init(44100,16,2,1024)
A continuación, almacenamos los diferentes sonidos del juego en las variables
come_fruta, pierde_vida y boom (su significado es evidente). Y lo último es definir la
función reproducir_sonido() que usaremos en otras partes del juego para hacer lo
propio. La forma de hacerlo es bastante divertida. En lugar de mirar de qué sonido se trata
con un grupo de if y elif, definimos un diccionario en el que las claves son los nombres
de los sonidos y los valores los sonidos mismos. Así, la instrucción
sonidos[nombre].play()
utiliza el diccionario para ejecutar el método play() del sonido adecuado. ¡Interesante!
PÁGINA 10 DE 27
CC: FERNANDO SALAMERO
11. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
banana.py
# -*- coding: utf-8 -*-
from pygame.sprite import Sprite
import util
class Banana(Sprite):
def __init__(self, x, y):
Sprite.__init__(self)
self.image = util.cargar_imagen('banana.png')
self.rect = self.image.get_rect()
self.rect.center = (x, y)
self.rect_colision = self.rect.inflate(-30, -10)
self.delay = 0
self.se_puede_comer = True
def update(self):
pass
def update_desaparecer(self):
self.delay -= 1
if self.delay < 1:
self.kill()
def comer(self):
self.image = util.cargar_imagen('banana_a_punto_de_comer.png')
self.delay = 30
self.update = self.update_desaparecer
self.se_puede_comer = False
El módulo banana se encarga de implementar la clase de sprites Banana. Para empezar
observa el detalle de que se declara como
class Banana(Sprite):
y no como
class Banana(pygame.sprite.Sprite):
ya que hemos importado la clase de la que deriva con from pygame.sprite import
Sprite en lugar de escribir simplemente import pygame.
Veamos las diferentes funciones miembro:
PÁGINA 11 DE 27
CC: FERNANDO SALAMERO
12. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
1. __init__()
El constructor de la clase toma dos argumentos, x e y, que indican dónde se va a
colocar el sprite (de hecho se centra allí a través de rect.center). También se define el
atributo rect_colision (al que nos hemos referido en el módulo util):
self.rect_colision = self.rect.inflate(-30, -10)
Fíjate en el uso del método inflate(). Si consultas la documentación de PyGame verás
que el efecto de la línea anterior es reducir el Rect que se usará para considerar
las colisiones en 30 pixeles horizontalmente y 10 pixeles verticalmente
(para ajustarlo más al propio dibujo de la banana).
Por último, dentro de esta función, se definen dos atributos que se usarán
posteriormente; delay y se_puede_comer.
2. update()
Si recuerdas que este método de los sprites se usa para calcular la nueva posición y si
caes en la cuenta de que las bananas están siempre quietas, comprenderás enseguida
que en update() no debe hacerse nada. Pero no podemos dejarlo en blanco (ya que
Python espera una línea sangrada con código tras el def); en estos casos se usa la
instrucción pass, cuyo objetivo es precisamente ese.
3. update_desaparecer()
Este método es muy interesante y la técnica empleada es muy útil. La idea es la
siguiente; imagina que quieres hacer desaparecer un sprite pero que no lo haga
inmediatamente si no con un cierto retardo (lo que permite cambiar su imagen o
modificar cualquier otra cosa). ¿Cómo implementarlo? La solución que vemos aquí es
usar un atributo, delay, e ir disminuyendo su valor hasta que se haga cero, en cuyo
caso eliminamos el sprite con kill(). Ahora sólo hay que conseguir que este método se
invoque en cada fotograma de la animación para que la cuenta atrás comience...
4. comer()
... lo que se implementa en este método, llamado comer(). Cuando el jugador alcance
una banana, se ejecuta este método (lo hemos visto antes, en la explicación de
monkeyhunter.py). Lo primero que se hace es cambiar el aspecto del sprite (¿has
visto que la banana empieza a pelarse?), luego se da el valor 30 al atributo delay y a
continuación se indica con
self.update = self.update_desaparecer
que el nuevo método update() del sprite será update_desaparecer(). ¡Genial!
Ahora, con cada llamada que se haga al update() de los sprites, la banana llamará a
update_desaparecer() y disminuirá, como hemos visto, el valor de delay en 1. Si la
animación la tenemos a 60 fps, en medio segundo se ejecutará su kill() y la banana
desaparecerá.
Observa también que hemos puesto se_puede_comer a False. El objetivo es que no
se solapen más colisiones y que PyGame no piense, mientras dura el retardo, que el
jugador está comiendo más banana y comience el proceso de nuevo (¿recuerdas
cuando los sprites de mario se quedaban ‘enganchados’?).
PÁGINA 12 DE 27
CC: FERNANDO SALAMERO
13. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
bomba.py
# -*- coding: utf-8 -*-
from pygame.sprite import Sprite
import util
class Bomba(Sprite):
def __init__(self, x, y):
Sprite.__init__(self)
self.cuadros = [
util.cargar_imagen('bomba1.png'),
util.cargar_imagen('bomba2.png'),
]
self.rect = self.cuadros[0].get_rect()
self.rect.center = (x, y)
self.esta_cerrada = False
self.rect_colision = self.rect.inflate(-30, -30)
self.paso = 0
self.delay = 0
def update(self):
if self.delay < 1:
self.actualizar_animacion()
self.delay = 3
else:
self.delay -= 1
def actualizar_animacion(self):
if self.paso == 0:
self.paso = 1
else:
self.paso = 0
self.image = self.cuadros[self.paso]
El módulo bomba se encarga de implementar la clase de sprites Bomba. Hay una
diferencia entre este tipo de sprites y los demás y es que es el único que, sin hacer nada,
tiene un aspecto animado (para simular la mecha encendida, la imagen del sprite va
cambiando entre dos; una con el fuego pequeño, bomba1.png y otra con el fuego grande,
bomba2.png). Sigamos el esquema habitual:
1. __init__()
El atributo cuadros es una lista con las dos imágenes que hemos citado. Inicialmente
queremos la primera, así que el atributo rect del sprite lo tomamos de ella:
PÁGINA 13 DE 27
CC: FERNANDO SALAMERO
14. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
self.rect = self.cuadros[0].get_rect()
Más cosas interesantes que podemos señalar; el atributo esta_cerrada tiene una
misión similar al se_puede_comer de los sprites de tipo Banana (evitar que se
detecte la colisión cuando ya se ha colisionado). También usamos el método inflate()
para ajustar el tamaño del rectángulo de colisión a la propia imagen y definimos delay
para gestionar que la bomba desaparezca no inmediatamente (para que dé tiempo a
mostrar la explosión).
Finalmente, el atributo paso se encargará de indicar cuál es la imagen de las dos que
se va a dibujar.
2. update()
Lo que hay que hacer aquí es gestionar el aspecto de la bomba (la posición no por que
no se mueve). En realidad son dos tareas; cambiar el dibujo y hacerlo al ritmo
adecuado. El ritmo lo marca delay. Observa el if: si delay es menor que 1 (como lo
es inicialmente) cambiamos el dibujo llamando al método actualizar_animacion()
y ponemos el valor de delay a 3. En caso contrario se le descuenta 1. El resultado de lo
anterior es que cada cuatro fotogramas de la animación (delay empieza en 3, luego 2,
luego 1 y finalmente es 0) se cambia el dibujo.
3. actualizar_animacion()
En este método hay que cambiar el dibujo, indicado con paso. Así, la línea
self.image = self.cuadros[self.paso]
actualiza el sprite con la imagen correcta, pues cuadros[0] es la primera imagen y
cuadros[1] la segunda. Al mismo tiempo, el valor de paso se cambia, de manera que
si valía 1 ahora pasa a valer 0 y viceversa. De ello se encarga el bloque if.
PÁGINA 14 DE 27
CC: FERNANDO SALAMERO
17. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
def ponerse_contento(self):
self.image = self.contento
self.delay = 60
El módulo cazador implementa la clase de sprites Cazador. Al contrario que las
bananas y las bombas, los cazadores se desplazan por el escenario, así que sus métodos
serán algo más complejos.
1. __init__()
Al igual que con el resto de los sprites, este método toma como parámetros las
coordenadas en las que queremos que se cree el sprite. Pero en este caso tenemos un
parámetro más, escenario, en el que se almacena una referencia al escenario (para
que podamos manipularlo y que, por ejemplo, el sprite sepa por dónde puede moverse
y por dónde no). Por cierto; para controlar el movimiento necesitamos poder indicar la
dirección. Para ello se definen las constantes IZQUIERDA, DERECHA, ARRIBA y
ABAJO como 0, 1, 2 y 3. ¿Ves cómo se ha usado range(4)?
Entre otras cosas que ya hemos comentado previamente, también encontramos:
a. Una llamada al método cargar_imagenes() que almacena las dos imágenes del
sprite en los atributos normal y contento, y a continuación se establece image
como normal (el aspecto inicial del cazador).
b. Utilizamos la función util.a_celdas() para tener convertida, como hemos indicado
antes, las posición del cazador a la cuadrícula del juego. Así definimos de un tirón
los atributos fila_destino y columna_destino.
c. El atributo demora_antes_de_mover se pone a 0. Luego veremos su
significado.
d. El movimiento inicial del sprite se adjudica hacia la IZQUIERDA.
2. cargar_imagenes()
Como hemos dicho, define los atributos normal y contento para usar la imagen
adecuada según sea el estado del cazador.
3. update()
Si piensas en una cuadrícula, mover hacia abajo, por ejemplo, es desplazarse cero
celdas en dirección horizontal y 1 celda en dirección vertical. Y de forma similar con el
resto de las direcciones. Es por ello por lo que al comienzo del método update() se
define un diccionario con el desplazamiento que corresponde a cada dirección:
direcciones = {
IZQUIERDA: (-1, 0),
DERECHA: (1, 0),
ARRIBA: (0, -1),
ABAJO: (0, 1)
}
Bien. A continuación hemos de mover de forma efectiva al cazador. Pero no queremos
PÁGINA 17 DE 27
CC: FERNANDO SALAMERO
18. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
que se mueva muy deprisa. La forma de hacerlo en el programa es un tanto
enrevesada, usando demora_antes_de_mover. ¿Te has fijado cómo se mueven los
cazadores cuando juegas? Dan un paso (con un movimiento suave) y hay una pequeña
pausa, vuelven a moverse y pausa y así sucesivamente. La clave está en
if self.demora_antes_de_mover < 1:
x, y = direcciones[self.direccion]
self.mover(x, y)
self.demora_antes_de_mover = 30
else:
self.actualizar_posicion()
Si demora_antes_de_mover ha llegado a cero, se elige una dirección, se llama al
método mover() y se pone de nuevo el valor de la demora a 30 para volver a esperar;
en caso contrario, se llama al método actualizar_posicion(). Fíjate en las
correspondientes funciones para tratar de entender el proceso.
En cualquier caso, a continuación, se invocan dos métodos;
actualizar_animacion() y actualizar_rect_colision() y se disminuye en una
unidad a demora_antes_de_mover. Veamos si analizando todos estos métodos
comprendemos el funcionamiento:
4. actualizar_posicion()
Recuerda que esta función se llama mientras el valor de demora_antes_de_mover
no ha alcanzado 0. El objetivo es el siguiente; hay que ir acercando el sprite desde su
posición actual hasta la almacenada en fila_destino y columna_destino. Para ello,
mira la distancia que le separa y avanza una doceava parte (siempre que no quede
menos de un pixel, es decir, que esa doceava parte no sea menor de 0.1). ¿Ves cómo
lo hace en el código? Esa doceava parte se llama delta_x (en la dirección horizontal) y
delta_y (en la vertical). ¿Ves también que para situar al sprite utiliza sus atributos
rect.center? Hay más sutilidades, pero por si no lo has notado, hecho de esta manera
se consigue que los pasos sean cada vez más pequeños y así parece que el cazador se va
frenando. El efecto queda suave y elegante.
5. mover()
mover(), sin embargo, se invoca cuando demora_antes_de_mover llega a 0. Los
dos parámetros que toma son desplazamiento_fila y
desplazamiento_columna, es decir, lo que debe moverse el sprite en la cuadrícula.
El objetivo de esta función es comprobar que el sprite puede moverse en esa dirección
en cuyo caso se cambian los valores de fila_destino y columna_destino. Si el
escenario no permite este movimiento en la posición actual, se cambia la dirección por
otra distinta para probar la próxima vez que se llame a este método.
¿Te has fijado que, para determinar si el cazador se puede mover por el escenario en la
dirección indicada, se usa el método puede_avanzar del objeto escenario? Luego
veremos cómo funciona.
6. actualizar_rect_colision()
Se invoca este método después de haber llamado a mover() para asegurarse que el
Rect que controla la colisión se mueve con el Rect del sprite adecuadamente. Observa
que se usan sus atributos midbottom de ambos para que valgan lo mismo.
7. actualizar_animacion()
Aquí tenemos el uso de delay que hemos visto en los otros tipos de sprite, Banana y
Bomba. Tras un pequeño intervalo de tiempo, cuando delay llega a 0, la imagen del
sprite se cambia (en su caso) a normal.
PÁGINA 18 DE 27
CC: FERNANDO SALAMERO
19. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
8. ponerse_contento()
Finalmente, este método es invocado cuando el mono tropieza con una bomba; al
explotar, los cazadores se ponen contentos... La función simplemente cambia la
imagen del sprite a contento y pone el valor de delay a 60 para que comience la
cuenta atrás hasta que termine la sonrisa.
escenario.py
# -*- coding: utf-8 -*-
import pygame
import util
from banana import Banana
from bomba import Bomba
from cazador import Cazador
class Escenario:
def __init__(self, nivel=1):
self.vertical = util.cargar_imagen('vertical.png')
self.horizontal = util.cargar_imagen('horizontal.png')
self.esquina_1 = util.cargar_imagen('esquina_1.png')
self.esquina_3 = util.cargar_imagen('esquina_3.png')
self.esquina_7 = util.cargar_imagen('esquina_7.png')
self.esquina_9 = util.cargar_imagen('esquina_9.png')
self.mapa = self.cargar_nivel(nivel)
def imprimir(self, fondo):
imagenes = {
'-': self.horizontal,
'|': self.vertical,
'1': self.esquina_1,
'3': self.esquina_3,
'7': self.esquina_7,
'9': self.esquina_9,
}
y=0
for fila in self.mapa:
x=0
for celda in fila:
if celda in imagenes:
pos = (60 + x * 48 - 30, 80 + y * 43 - 30)
fondo.blit(imagenes[celda], pos)
x += 1
PÁGINA 19 DE 27
CC: FERNANDO SALAMERO
20. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
y += 1
def cargar_nivel(self, nivel):
nombre_del_archivo = 'nivel_%d.txt' %nivel
archivo = open(nombre_del_archivo, 'rt')
mapa = archivo.readlines()
archivo.close()
return mapa
def crear_objetos(self, bananas, bombas, cazadores):
y=0
for fila in self.mapa:
x=0
for celda in fila:
pos_x, pos_y = util.a_coordenadas(y, x)
if celda == '+':
bananas.add(Banana(pos_x, pos_y))
elif celda == 'x':
bombas.add(Bomba(pos_x, pos_y))
elif celda == '@':
cazadores.add(Cazador(pos_x, pos_y, self))
x += 1
y += 1
def puede_avanzar(self, (fila, columna), (df, dc)):
if fila + df < 0 or fila + df > 7:
return False
elif columna + dc < 0 or columna + dc > 11:
return False
if self.mapa[fila + df][columna + dc] in '1379-|':
return False
return True
Ya que el sprite de tipo Cazador hace referencia a la clase Escenario, no vamos a
retrasarlo más y vamos a ver cómo está implementada. Ten en cuenta que incluye un
laberinto (cuyo tamaño es de 550x320 pixeles y que está más o menos centrado en
pantalla, a 50 pixeles del borde izquierdo y 80 pixeles del borde derecho). No nos vale que
el laberinto sea simplemente una imagen de fondo, pues necesitamos saber vía código por
dónde puede moverse un sprite y por dónde no. La manera de atacar el problema es
construir el laberinto a base de ‘ladrillos’, es decir, tener codificada la disposición de las
paredes y dibujarlas consecuentemente. Dicha codificación está en el archivo de texto
nivel_1.txt. Cambiar el ‘mapa’ o añadir otros nuevos es así, sencillo.
PÁGINA 20 DE 27
CC: FERNANDO SALAMERO
21. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
1. __init()__
Lo primero es observar que Escenario no es una clase derivada de la clase Sprite de
PyGame. En el método constructor de la clase simplemente se cargan las imágenes que
usaremos en el dibujado del laberinto; vertical, horizontal, esquina_1,
esquina_3, esquina_7 y esquina_9. ¿Te sorprenden los nombres? Enseguida lo
entenderás. También se llama al método cargar_nivel() y se almacena el resultado
en el atributo mapa (éste es precisamente el mapa de nuestro laberinto, como
veremos).
2. imprimir()
Este método tiene el propósito de dibujar en pantalla (más concretamente, sobre la
Surface que se le pasa como argumento) el laberinto. Para acceder con más facilidad a
las imágenes que se usan, lo primero que se define es un diccionario con ellas
denominado imagenes. Sus valores son las imágenes correspondientes a las claves,
que son la forma en la que están codificadas en el atributo mapa; un texto de varias
líneas en el que cada carácter es una celda de la cuadrícula. Si miras el archivo
nivel_1.txt lo comprenderás. ¿Ves los caracteres ‘-’, ‘|’, ‘1’, ‘3’, ‘7’ y ‘9’? ¿Entiendes
ahora por qué tienen las imágenes los nombres que tienen?
Para dibujar el mapa hay que recorrer, entonces, dicho texto mirando cada uno de los
caracteres y dibujando en pantalla la imagen correspondiente en el lugar
correspondiente. En el bucle for subsiguiente, las variables x e y se emplean para
calcular la posición en pixeles donde hay que poner la imagen. Nuevamente, intenta
entender la parte matemática; es algo similar a la función a_coordenadas() del
módulo util. Fíjate también que hay que asegurarse que el carácter es dibujable (en el
texto hay espacios en blanco y otros caracteres...); es por ello por lo que necesitamos
if celda in imagenes:
antes de pasar a dibujar realmente con la función blit().
3. cargar_nivel()
Este método lee un archivo de texto (el nivel cuyo número se le pasa como parámetro)
y lo devuelve como resultado. La forma de hacerlo es muy sencilla;
a. Primero el archivo se abre con la función open(). El parámetro ‘rt’ hace que se
abra como sólo lectura (consulta la documentación de Python). Observa la forma
de construir, en la línea anterior, el nombre del archivo; se usa la función % para
cadenas de texto (de nuevo, acude a la documentación).
b. En segundo lugar, se leen todas las líneas del texto con el método readlines() del
objeto resultante y se almacenan en mapa.
c. Para finalizar, se cierra el archivo con el método close().
4. crear_objetos()
El funcionamiento de este método es muy similar al de imprimir(), sólo que esta vez
en lugar de dibujar en pantalla las paredes, a medida que se recorre el mapa se añaden
a los grupos adecuados los objetos codificados en él.
Al método hay que pasarle tres grupos (para poderles añadir los sprites). Cuando,
mirando los diferentes caracteres del texto almacenado en mapa, se encuentra un ‘+’
se crea un sprite de tipo Banana y se añade al grupo bananas; cuando se encuentra
una ‘x’ se crea un sprite de tipo Bombas y se añade al grupo bombas; y lo mismo
sucede con el grupo cazadores cuando el carácter implicado es una ‘@’.
¿Ves qué sencillo es crear nuevos niveles? Sólo hay que crear un archivo de texto y
PÁGINA 21 DE 27
CC: FERNANDO SALAMERO
22. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
poner los correspondientes caracteres (paredes, enemigos, etc) en las posiciones
adecuadas.
5. puede_avanzar()
El último método de esta clase es el que se encarga de averiguar si el movimiento que
se le pasa como argumento es posible. Los sprites de tipo Cazador (como ya hemos
visto) y Mono invocan a este método antes de realizar un desplazamiento; es la forma
de saber si se va a chocar con una pared o se va a salir de la ventana. El método
devuelve True en caso de que sea posible, desde (fila, columna), desplazarse en la
dirección (df, dc). Este último valor sabemos que será (-1, 0), (1, 0), (0, -1) o (0, 1)
según se haya elegido en su momento IZQUIERDA, DERECHA, ARRIBA o
ABAJO.
La manera de implementarlo es sencilla. Se devuelve True (es decir, ‘el movimiento es
posible’) a no ser que se cumplan ciertas condiciones, en cuyo caso se devuelve False
(‘el movimiento no es posible’). Esas condiciones que impiden el movimiento se miran
con un bloque if; las dos primeras son los casos en los que el movimiento sacaría al
sprite de la ventana (es decir, de la cuadrícula; tiene 8 filas y 12 columnas que se
numeran empezando por 0) y la tercera condición utiliza el truco de Python para
averiguar si un carácter está dentro de un texto:
x in '1379-|'
lo que haría es devolver True en el caso de que x contenga un carácter ‘1’, ‘3’, ‘7’, ‘9’,
‘-’ o ‘’|’ y False en caso contrario. Así que esa última condición mira si en la posición a
la que se quiere mover está una de las paredes y, por tanto, el movimiento es
imposible.
PÁGINA 22 DE 27
CC: FERNANDO SALAMERO
23. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
explosion.py
# -*- coding: utf-8 -*-
import pygame
from pygame.sprite import Sprite
import util
class Explosion(Sprite):
def __init__(self, bomba_que_explota):
Sprite.__init__(self)
self.cuadros = [
util.cargar_imagen('explosion1.png'),
util.cargar_imagen('explosion2.png')]
self.delay = 10
self.rect = pygame.Rect(bomba_que_explota.rect)
self.rect.center = bomba_que_explota.rect.topleft
self.contador = 0
def update(self):
self.contador -= 1
if self.contador < 0:
self.image = self.cuadros[self.delay % 2]
self.delay -= 1
if self.delay < 0:
self.kill()
self.contador = 2
El módulo explosion implementa la clase Explosion, es decir, el sprite que aparece
momentáneamente cuando el mono protagonista choca con una bomba.
1. __init__()
El método constructor de la clase debería resultar muy familiar; es una mezcla de los
de las clases Banana y Bomba. Definimos el atributo cuadros (que es una lista con
las dos imágenes de la explosión), centramos el atributo rect del sprite con el Rect de
la primera imagen y ponemos delay a 10 (cuando se crea el sprite comienza
inmediatamente la cuenta atrás para que desaparezca la animación de la explosión) y
contador a 0 (que indicará cuando hay que cambiar la imagen).
2. update()
Este método, que se encarga de ir cambiando la imagen de la explosión, podría
implementarse de forma muy similar a los de los otros sprites. Sin embargo, las
pequeñas variaciones que vamos a ver te pueden enseñar/refrescar alguna técnica
más.
Se empieza restando 1 a contador. A continuación miramos si contador es negativo
(como ocurre al principio) y en tal caso se cambia la imagen del sprite con la línea
PÁGINA 23 DE 27
CC: FERNANDO SALAMERO
24. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
self.image = self.cuadros[self.delay % 2]
Puede que te parezca extraña. Pero, en definitiva, self.delay%2 lo que hace (si
recuerdas) es calcular el resto de dividir el valor de delay por 2. El resultado
siempre es o 0 o 1, con lo que se almacenará en la imagen del sprite cuadros[0] o
cuadros[1], alternativamente (que es lo que deseamos; mostrar las dos imágenes
para producir la animación de la explosión).
A continuación, se disminuye en una unidad a delay y en el caso de que éste sea
negativo se procede a eliminar el sprite con el método kill(). Si el sprite aún no ha de
desaparecer, contador se pone a 2 para regular la velocidad con la que cambia la
imagen en la animación.
mono.py
# -*- coding: utf-8 -*-
import pygame
from pygame.sprite import Sprite
from pygame import *
import util
import sonidos
class Mono(Sprite):
def __init__(self, escenario):
Sprite.__init__(self)
self.cargar_imagenes()
self.image = self.normal
self.escenario = escenario
self.en_movimiento = True
self.columna_destino = 0
self.fila_destino = 3
self.delay = 0
self.x = -50
self.y = 209
self.rect = self.image.get_rect()
self.rect_colision = self.rect.inflate(-30, -30)
def cargar_imagenes(self):
self.normal = util.cargar_imagen('mono.png')
self.contento = util.cargar_imagen('mono_contento.png')
self.pierde = util.cargar_imagen('mono_pierde.png')
def update(self):
if not self.en_movimiento:
teclas = pygame.key.get_pressed()
PÁGINA 24 DE 27
CC: FERNANDO SALAMERO
26. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
if self.delay < 1:
self.image = self.normal
def ponerse_contento(self):
self.image = self.contento
self.delay = 30
sonidos.reproducir_sonido('come')
def pierde_una_vida(self):
self.image = self.pierde
self.delay = 65
Por fin llegamos al módulo mono que implementa la clase Mono, el personaje del
jugador. Analicemos el último fragmento del código del juego:
1. __init__()
El mono se desplaza por el laberinto igual que los cazadores, así que comparten
bastantes atributos y métodos. Hay unas pocas diferencias. Fíjate que queremos que el
jugador comience siempre en la misma posición y no tenemos (como con los
cazadores) que leer su posición desde el archivo de texto del nivel correspondiente. Por
ello __init__() sólo tiene como argumento una referencia al escenario y se ponen a
mano los valores de los atributos fila_destino, columna_destino, x e y. También
se define el atributo en_movimiento con el valor True (cuyo objetivo lo veremos
enseguida).
2. cargar_imagenes()
Poco que decir aquí. Las tres imágenes del juego son normal, contento (para cuando
come una banana) y pierde (cuando choca con una bomba).
3. update()
La técnica aquí es muy interesante, sobre todo es aplicable cuando se tienen en
pantalla múltiples sprites que pueden ser manejados por diferentes teclas y diferentes
jugadores. En lugar de mirar las teclas pulsadas en el cuerpo principal del programa,
es mucho más eficiente que cada sprite mire si se han pulsado las teclas que
controlan su propio movimiento; como quiera que en cada ejecución del bucle de
la animación se llama a los métodos update() de todos los sprites, cada uno de ellos
vigilará independientemente si se han pulsado las teclas que le importan.
Precisamente por ello hay que tener cuidado de parar al sprite cuando no procede
moverlo y ésa es la razón por la que se usa el atributo en_movimiento. Así que este
método comienza verificando que su valor es False para poder iniciar el movimiento
del sprite. El movimiento es algo complejo, pero recuerda que es por que movemos al
personaje en una cuadrícula a golpes, de celda en celda. En otro tipo de juego, bastaría
con desplazar sin más al sprite según sean las teclas pulsadas.
En cualquier caso, de forma pareja a la clase Cazador, si en_movimiento es True
se llama al método actualizar_posicion(). Y en ambos casos, se invocan también los
métodos actualizar_animacion() y actualiar_rect_colision().
PÁGINA 26 DE 27
CC: FERNANDO SALAMERO
27. PROGRAMA: MONKEY HUNTER
CURSO: 1º BACHILLERATO
4. actualizar_posicion()
El método es prácticamente idéntico al de los sprites de tipo Cazador, con dos
pequeños matices. El primero es que el movimiento es más rápido (para que el mono
responda al jugador más ágilmente) y por ello se divide por 2.5 en lugar de por 12 (al
haber menos pasos intermedios, se mueve más deprisa). El segundo matiz es que
cuando el mono llega realmente a su destino se pone el valor de en_movimiento a
False.
5. mover()
mover() es mucho más sencillo, esta vez. Lo único que se hace es mirar dónde está el
mono (pos_actual) y cuál va a ser el desplazamiento (desplazamiento), comprobar
si es posible usando el método escenario.puede_avanzar() y, en tal caso, cambiar
los valores de fila_destino y columna_destino a los nuevos.
6. actualizar_rect_colision()
Exáctamente el mismo que el de la clase Cazador.
7. actualizar_animacion()
También es idéntico al de la clase Cazador.
8. ponerse_contento()
Para alegrar al mono cuando come una banana, se cambia la imagen del sprite a
contento, se pone el valor de delay a 30 para que comience la cuenta atrás para
volver a la imagen normal y se reproduce el sonido correspondiente usando la función
reproducir_sonidos() del módulo sonidos.
9. pierde_una_vida()
Este método es muy importante en un juego real (donde se harían muchos más
ajustes). En nuestro programa simplemente se cambia la imagen a pierde y se pone
delay a 65.
PÁGINA 27 DE 27
CC: FERNANDO SALAMERO