Programación con Pygame VIII

27
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 (41 m 59 s ), 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: PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO PÁGINA 1 DE 27 CC: FERNANDO SALAMERO

description

Los archivos pueden encontrarse en http://sites.google.com/site/laislalibre/informatica/python/pygame

Transcript of Programación con Pygame VIII

Page 1: Programación con Pygame VIII

Monkey Hunter: Plataformas

(Código de Hugo Ruscitti, http://www.losersjuegos.com.ar ligeramente modificado)

IntroducciónMonkey 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:

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 1 DE 27 CC: FERNANDO SALAMERO

Page 2: Programación con Pygame VIII

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.pyEl programa principal y el que hay que ejecutar para lanzar el juego.

2. util.pyContiene funciones útiles para realizar diferentes tareas.

3. sonidos.pyDefine los diferentes sonidos del juego y cómo ejecutarlos.

4. mono.pyContiene la clase Mono, derivada de la clase pygame.sprite.Sprite.

5. cazador.pySe trata de la clase Cazador (los enemigos), otro sprite.

6. bomba.pyclase Bomba que representa los sprites de las bombas.

7. banana.pyLas bananas son objetos de la clase Banana, implementados en este archivo.

8. escenario.pyLa clase Escenario controla el laberinto del juego No es un sprite.

# -*- coding: utf-8 -*-## Copyright 2007 Hugo Ruscitti <[email protected]># 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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 2 DE 27 CC: FERNANDO SALAMERO

Page 3: Programación con Pygame VIII

9. explosion.pyLa 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 pygameimport utilimport sonidos

from mono import Monofrom explosion import Explosionfrom 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 = Falsereloj = pygame.time.Clock()

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 3 DE 27 CC: FERNANDO SALAMERO

Page 4: Programación con Pygame VIII

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).

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()

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 4 DE 27 CC: FERNANDO SALAMERO

Page 5: Programación con Pygame VIII

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)

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 5 DE 27 CC: FERNANDO SALAMERO

Page 6: Programación con Pygame VIII

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.

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 6 DE 27 CC: FERNANDO SALAMERO

Page 7: Programación con Pygame VIII

¿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().

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 7 DE 27 CC: FERNANDO SALAMERO

Page 8: Programación con Pygame VIII

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

En el módulo util definimos una serie de funciones que se emplean en el resto del juego. Éstas son:

import osimport pygamefrom 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)

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 8 DE 27 CC: FERNANDO SALAMERO

Page 9: Programación con Pygame VIII

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.

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 9 DE 27 CC: FERNANDO SALAMERO

Page 10: Programación con Pygame VIII

sonidos.py

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!

import pygameimport 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()

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 10 DE 27 CC: FERNANDO SALAMERO

Page 11: Programación con Pygame VIII

banana.py

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:

# -*- coding: utf-8 -*-

from pygame.sprite import Spriteimport 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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 11 DE 27 CC: FERNANDO SALAMERO

Page 12: Programación con Pygame VIII

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’?).

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 12 DE 27 CC: FERNANDO SALAMERO

Page 13: Programación con Pygame VIII

bomba.py

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:

# -*- coding: utf-8 -*-

from pygame.sprite import Spriteimport 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]

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 13 DE 27 CC: FERNANDO SALAMERO

Page 14: Programación con Pygame VIII

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.

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 14 DE 27 CC: FERNANDO SALAMERO

Page 15: Programación con Pygame VIII

cazador.py

# -*- coding: utf-8 -*-

import pygamefrom pygame.sprite import Spritefrom pygame import *import utilimport sonidos

# direccionesIZQUIERDA, DERECHA, ARRIBA, ABAJO = range(4)

class Cazador(Sprite):

def __init__(self, pos_x, pos_y, escenario): Sprite.__init__(self) self.cargar_imagenes() self.image = self.normal self.escenario = escenario self.delay = 0 self.x = pos_x self.y = pos_y self.fila_destino, self.columna_destino = util.a_celdas(pos_x, pos_y) self.demora_antes_de_mover = 0 self.rect = self.image.get_rect() self.rect.center = (pos_x, pos_y) self.rect_colision = self.rect.inflate(-30, -30) self.direccion = IZQUIERDA

def cargar_imagenes(self): self.normal = util.cargar_imagen('cazador.png') self.contento = util.cargar_imagen('cazador_contento.png')

def update(self): direcciones = { IZQUIERDA: (-1, 0), DERECHA: (1, 0), ARRIBA: (0, -1), ABAJO: (0, 1) }

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()

self.actualizar_animacion()

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 15 DE 27 CC: FERNANDO SALAMERO

Page 16: Programación con Pygame VIII

self.actualizar_rect_colision() self.demora_antes_de_mover -= 1

def actualizar_posicion(self): pos = util.a_coordenadas(self.fila_destino, self.columna_destino) destino_x, destino_y = pos

delta_x = (destino_x - self.x) / 12.0 delta_y = (destino_y - self.y) / 12.0

if abs(delta_x) < 0.1 and abs(delta_y) < 0.1: self.x = destino_x self.y = destino_y else: self.x += delta_x self.y += delta_y

self.rect.centerx = int(self.x) self.rect.centery = int(self.y)

def mover(self, desplazamiento_columna, desplazamiento_fila): pos_actual = (self.fila_destino, self.columna_destino) desplazamiento = (desplazamiento_fila, desplazamiento_columna)

if self.escenario.puede_avanzar(pos_actual, desplazamiento): self.fila_destino += desplazamiento_fila self.columna_destino += desplazamiento_columna else: if self.direccion == IZQUIERDA: self.direccion = ARRIBA elif self.direccion == ARRIBA: self.direccion = DERECHA elif self.direccion == DERECHA: self.direccion = ABAJO elif self.direccion == ABAJO: self.direccion = IZQUIERDA

def actualizar_rect_colision(self): self.rect_colision.midbottom = self.rect.midbottom

def actualizar_animacion(self): if self.delay > 0: self.delay -= 1

if self.delay < 1: self.image = self.normal

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 16 DE 27 CC: FERNANDO SALAMERO

Page 17: Programación con Pygame VIII

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

def ponerse_contento(self): self.image = self.contento self.delay = 60

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 17 DE 27 CC: FERNANDO SALAMERO

Page 18: Programación con Pygame VIII

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 = 30else: 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.

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 18 DE 27 CC: FERNANDO SALAMERO

Page 19: Programación con Pygame VIII

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 pygameimport utilfrom banana import Bananafrom bomba import Bombafrom 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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 19 DE 27 CC: FERNANDO SALAMERO

Page 20: Programación con Pygame VIII

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.

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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 20 DE 27 CC: FERNANDO SALAMERO

Page 21: Programación con Pygame VIII

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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 21 DE 27 CC: FERNANDO SALAMERO

Page 22: Programación con Pygame VIII

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.

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 22 DE 27 CC: FERNANDO SALAMERO

Page 23: Programación con Pygame VIII

explosion.py

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

# -*- coding: utf-8 -*-

import pygamefrom pygame.sprite import Spriteimport 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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 23 DE 27 CC: FERNANDO SALAMERO

Page 24: Programación con Pygame VIII

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 pygamefrom pygame.sprite import Spritefrom pygame import *import utilimport 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()

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 24 DE 27 CC: FERNANDO SALAMERO

Page 25: Programación con Pygame VIII

if teclas[K_LEFT]: self.mover(-1, 0) elif teclas[K_RIGHT]: self.mover(+1, 0) elif teclas[K_UP]: self.mover(0, -1) elif teclas[K_DOWN]: self.mover(0, +1) else: self.actualizar_posicion()

self.actualizar_animacion() self.actualizar_rect_colision()

def actualizar_posicion(self): pos = util.a_coordenadas(self.fila_destino, self.columna_destino) destino_x, destino_y = pos

delta_x = (destino_x - self.x) / 2.5 delta_y = (destino_y - self.y) / 2.5

if abs(delta_x) < 0.1 and abs(delta_y) < 0.1: self.x = destino_x self.y = destino_y self.en_movimiento = False else: self.x += delta_x self.y += delta_y

self.rect.centerx = int(self.x) self.rect.centery = int(self.y)

def mover(self, desplazamiento_columna, desplazamiento_fila): self.en_movimiento = True

pos_actual = (self.fila_destino, self.columna_destino) desplazamiento = (desplazamiento_fila, desplazamiento_columna)

if self.escenario.puede_avanzar(pos_actual, desplazamiento): self.fila_destino += desplazamiento_fila self.columna_destino += desplazamiento_columna

def actualizar_rect_colision(self): self.rect_colision.midbottom = self.rect.midbottom

def actualizar_animacion(self): if self.delay > 0: self.delay -= 1

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 25 DE 27 CC: FERNANDO SALAMERO

Page 26: Programación con Pygame VIII

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().

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

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 26 DE 27 CC: FERNANDO SALAMERO

Page 27: Programación con Pygame VIII

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.

PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO

PÁGINA 27 DE 27 CC: FERNANDO SALAMERO