SlideShare una empresa de Scribd logo
1 de 27
Descargar para leer sin conexión
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
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
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
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
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
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
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
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
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
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
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
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
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
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
PROGRAMA: MONKEY HUNTER
                     
                       CURSO: 1º BACHILLERATO


cazador.py

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

   import pygame
   from pygame.sprite import Sprite
   from pygame import *
   import util
   import sonidos

   # direcciones
   IZQUIERDA, 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()




PÁGINA 15 DE 27
                             
                    CC: FERNANDO SALAMERO
PROGRAMA: MONKEY HUNTER
                     
                     CURSO: 1º BACHILLERATO



         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




PÁGINA 16 DE 27
                             
                    CC: FERNANDO SALAMERO
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
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
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
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
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
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
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
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
PROGRAMA: MONKEY HUNTER
                        
                  CURSO: 1º BACHILLERATO



            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




PÁGINA 25 DE 27
                                
                 CC: FERNANDO SALAMERO
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
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

Más contenido relacionado

La actualidad más candente

Fases del proceso de programación
Fases del proceso de programaciónFases del proceso de programación
Fases del proceso de programaciónelizabethpaola
 
" 모바일 롤플레잉게임 인터페이스디자인 문서"
 " 모바일 롤플레잉게임 인터페이스디자인 문서" " 모바일 롤플레잉게임 인터페이스디자인 문서"
" 모바일 롤플레잉게임 인터페이스디자인 문서"multiwriter
 
게임 인공지능 설계
게임 인공지능 설계게임 인공지능 설계
게임 인공지능 설계ByungChun2
 
Game Programming 07 - Procedural Content Generation
Game Programming 07 - Procedural Content GenerationGame Programming 07 - Procedural Content Generation
Game Programming 07 - Procedural Content GenerationNick Pruehs
 
Presentación desarrollo de videojuegos
Presentación desarrollo de videojuegosPresentación desarrollo de videojuegos
Presentación desarrollo de videojuegosKayreKampa
 
Natural Game Design: How to Birth Games Without Cloning
Natural Game Design: How to Birth Games Without CloningNatural Game Design: How to Birth Games Without Cloning
Natural Game Design: How to Birth Games Without CloningGreg Costikyan
 
위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점Ryan Park
 
Video Game Design: Art & Sound
Video Game Design: Art & SoundVideo Game Design: Art & Sound
Video Game Design: Art & SoundKevin Duggan
 
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016Codemotion
 
복귀 이벤트를 해도 유저가 돌아오지 않는 이유
복귀 이벤트를 해도 유저가 돌아오지 않는 이유복귀 이벤트를 해도 유저가 돌아오지 않는 이유
복귀 이벤트를 해도 유저가 돌아오지 않는 이유Chanman Jo
 
KGC2010 김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템
KGC2010   김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템KGC2010   김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템
KGC2010 김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템Jubok Kim
 
게임 기획자의 생존 전략
게임 기획자의 생존 전략게임 기획자의 생존 전략
게임 기획자의 생존 전략태성 이
 
NDC 2018 레벨 디자인 튜토리얼 Level Design Tutorial
NDC 2018 레벨 디자인 튜토리얼 Level Design TutorialNDC 2018 레벨 디자인 튜토리얼 Level Design Tutorial
NDC 2018 레벨 디자인 튜토리얼 Level Design Tutorial용태 이
 
Tic Tac Toe Java Development
Tic Tac Toe Java DevelopmentTic Tac Toe Java Development
Tic Tac Toe Java Developmentpengqia chen
 
Introduction to Unity3D Game Engine
Introduction to Unity3D Game EngineIntroduction to Unity3D Game Engine
Introduction to Unity3D Game EngineMohsen Mirhoseini
 

La actualidad más candente (20)

Rust vs C++
Rust vs C++Rust vs C++
Rust vs C++
 
World building part 1
World building part 1World building part 1
World building part 1
 
Fases del proceso de programación
Fases del proceso de programaciónFases del proceso de programación
Fases del proceso de programación
 
" 모바일 롤플레잉게임 인터페이스디자인 문서"
 " 모바일 롤플레잉게임 인터페이스디자인 문서" " 모바일 롤플레잉게임 인터페이스디자인 문서"
" 모바일 롤플레잉게임 인터페이스디자인 문서"
 
게임 인공지능 설계
게임 인공지능 설계게임 인공지능 설계
게임 인공지능 설계
 
Game Programming 07 - Procedural Content Generation
Game Programming 07 - Procedural Content GenerationGame Programming 07 - Procedural Content Generation
Game Programming 07 - Procedural Content Generation
 
Level Design
Level DesignLevel Design
Level Design
 
Presentación desarrollo de videojuegos
Presentación desarrollo de videojuegosPresentación desarrollo de videojuegos
Presentación desarrollo de videojuegos
 
Natural Game Design: How to Birth Games Without Cloning
Natural Game Design: How to Birth Games Without CloningNatural Game Design: How to Birth Games Without Cloning
Natural Game Design: How to Birth Games Without Cloning
 
위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점
 
Video Game Design: Art & Sound
Video Game Design: Art & SoundVideo Game Design: Art & Sound
Video Game Design: Art & Sound
 
02 - Emergence and Progression
02 - Emergence and Progression02 - Emergence and Progression
02 - Emergence and Progression
 
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
 
복귀 이벤트를 해도 유저가 돌아오지 않는 이유
복귀 이벤트를 해도 유저가 돌아오지 않는 이유복귀 이벤트를 해도 유저가 돌아오지 않는 이유
복귀 이벤트를 해도 유저가 돌아오지 않는 이유
 
KGC2010 김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템
KGC2010   김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템KGC2010   김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템
KGC2010 김주복, 김충효 - M2 프로젝트의 절차적 리깅 시스템
 
게임 기획자의 생존 전략
게임 기획자의 생존 전략게임 기획자의 생존 전략
게임 기획자의 생존 전략
 
NDC 2018 레벨 디자인 튜토리얼 Level Design Tutorial
NDC 2018 레벨 디자인 튜토리얼 Level Design TutorialNDC 2018 레벨 디자인 튜토리얼 Level Design Tutorial
NDC 2018 레벨 디자인 튜토리얼 Level Design Tutorial
 
God Of War : post mortem
God Of War : post mortemGod Of War : post mortem
God Of War : post mortem
 
Tic Tac Toe Java Development
Tic Tac Toe Java DevelopmentTic Tac Toe Java Development
Tic Tac Toe Java Development
 
Introduction to Unity3D Game Engine
Introduction to Unity3D Game EngineIntroduction to Unity3D Game Engine
Introduction to Unity3D Game Engine
 

Destacado

(Sin anotaciones) - En busca de la Física
(Sin anotaciones) - En busca de la Física(Sin anotaciones) - En busca de la Física
(Sin anotaciones) - En busca de la FísicaFernando Salamero
 
Programación de Videojuegos con Python y Pilas (III)
Programación de Videojuegos con Python y Pilas (III)Programación de Videojuegos con Python y Pilas (III)
Programación de Videojuegos con Python y Pilas (III)Fernando Salamero
 
Programación de Videojuegos con Python y Pilas (I)
Programación de Videojuegos con Python y Pilas (I)Programación de Videojuegos con Python y Pilas (I)
Programación de Videojuegos con Python y Pilas (I)Fernando Salamero
 
Programación de Videojuegos con Python y Pilas (VI)
Programación de Videojuegos con Python y Pilas (VI)Programación de Videojuegos con Python y Pilas (VI)
Programación de Videojuegos con Python y Pilas (VI)Fernando Salamero
 
Programación de Videojuegos con Python y Pilas (II)
Programación de Videojuegos con Python y Pilas (II)Programación de Videojuegos con Python y Pilas (II)
Programación de Videojuegos con Python y Pilas (II)Fernando Salamero
 

Destacado (8)

Programación con Pygame IX
Programación con Pygame IXProgramación con Pygame IX
Programación con Pygame IX
 
Pythonic Math
Pythonic MathPythonic Math
Pythonic Math
 
(Sin anotaciones) - En busca de la Física
(Sin anotaciones) - En busca de la Física(Sin anotaciones) - En busca de la Física
(Sin anotaciones) - En busca de la Física
 
Programación de Videojuegos con Python y Pilas (III)
Programación de Videojuegos con Python y Pilas (III)Programación de Videojuegos con Python y Pilas (III)
Programación de Videojuegos con Python y Pilas (III)
 
Programación de Videojuegos con Python y Pilas (I)
Programación de Videojuegos con Python y Pilas (I)Programación de Videojuegos con Python y Pilas (I)
Programación de Videojuegos con Python y Pilas (I)
 
Iniciación a python
Iniciación a pythonIniciación a python
Iniciación a python
 
Programación de Videojuegos con Python y Pilas (VI)
Programación de Videojuegos con Python y Pilas (VI)Programación de Videojuegos con Python y Pilas (VI)
Programación de Videojuegos con Python y Pilas (VI)
 
Programación de Videojuegos con Python y Pilas (II)
Programación de Videojuegos con Python y Pilas (II)Programación de Videojuegos con Python y Pilas (II)
Programación de Videojuegos con Python y Pilas (II)
 

Similar a Programación con Pygame VIII

Programación con Pygame (II)
Programación con Pygame (II)Programación con Pygame (II)
Programación con Pygame (II)Fernando Salamero
 
2. tutorial unity3d-disparo
2.  tutorial unity3d-disparo2.  tutorial unity3d-disparo
2. tutorial unity3d-disparoVictor Aravena
 
2. tutorial unity3d-disparo
2.  tutorial unity3d-disparo2.  tutorial unity3d-disparo
2. tutorial unity3d-disparoVictor Aravena
 
Programación de Videojuegos con Python y Pilas (X)
Programación de Videojuegos con Python y Pilas (X)Programación de Videojuegos con Python y Pilas (X)
Programación de Videojuegos con Python y Pilas (X)Fernando Salamero
 
Programación de Videojuegos con Python y Pilas (VII)
Programación de Videojuegos con Python y Pilas (VII)Programación de Videojuegos con Python y Pilas (VII)
Programación de Videojuegos con Python y Pilas (VII)Fernando Salamero
 
Programación con Pygame III
Programación con Pygame IIIProgramación con Pygame III
Programación con Pygame IIIFernando Salamero
 
6. revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame
6.  revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame6.  revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame
6. revisión y modificación del juego -“space blaster”- Construct 5 - VideoGameVictor Aravena
 
Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...
Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...
Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...Telefónica
 
EXPOSICION PYGAME
EXPOSICION PYGAMEEXPOSICION PYGAME
EXPOSICION PYGAMEdj.l
 
Programación de Videojuegos con Python y Pilas (V)
Programación de Videojuegos con Python y Pilas (V)Programación de Videojuegos con Python y Pilas (V)
Programación de Videojuegos con Python y Pilas (V)Fernando Salamero
 
Desarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdf
Desarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdfDesarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdf
Desarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdfFidelPaedaMartnez
 
Develop Mobile Apps with Corona SDK
Develop Mobile Apps with Corona SDKDevelop Mobile Apps with Corona SDK
Develop Mobile Apps with Corona SDKAlberto Dominguez
 
Como instalar Python y librerias
Como instalar Python y libreriasComo instalar Python y librerias
Como instalar Python y libreriasDaniel Iba
 
Codigo ejemplo j2 me
Codigo ejemplo   j2 meCodigo ejemplo   j2 me
Codigo ejemplo j2 meOscar Eduardo
 
Codemotion 2014 - Introducción a Unity
Codemotion 2014 - Introducción a UnityCodemotion 2014 - Introducción a Unity
Codemotion 2014 - Introducción a UnityMiguelitoCupra
 

Similar a Programación con Pygame VIII (20)

Programación con Pygame (II)
Programación con Pygame (II)Programación con Pygame (II)
Programación con Pygame (II)
 
Programación con Pygame IV
Programación con Pygame IVProgramación con Pygame IV
Programación con Pygame IV
 
2. tutorial unity3d-disparo
2.  tutorial unity3d-disparo2.  tutorial unity3d-disparo
2. tutorial unity3d-disparo
 
2. tutorial unity3d-disparo
2.  tutorial unity3d-disparo2.  tutorial unity3d-disparo
2. tutorial unity3d-disparo
 
Programación de Videojuegos con Python y Pilas (X)
Programación de Videojuegos con Python y Pilas (X)Programación de Videojuegos con Python y Pilas (X)
Programación de Videojuegos con Python y Pilas (X)
 
Intro Pygame Capitulo 2
Intro Pygame Capitulo 2Intro Pygame Capitulo 2
Intro Pygame Capitulo 2
 
Programación de Videojuegos con Python y Pilas (VII)
Programación de Videojuegos con Python y Pilas (VII)Programación de Videojuegos con Python y Pilas (VII)
Programación de Videojuegos con Python y Pilas (VII)
 
Intro pygamev2
Intro pygamev2Intro pygamev2
Intro pygamev2
 
Programación con Pygame III
Programación con Pygame IIIProgramación con Pygame III
Programación con Pygame III
 
6. revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame
6.  revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame6.  revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame
6. revisión y modificación del juego -“space blaster”- Construct 5 - VideoGame
 
Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...
Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...
Explotar Eternalblue & Doublepulsar para obener una shell de Empire/Meterpret...
 
Programación con Pygame V
Programación con Pygame VProgramación con Pygame V
Programación con Pygame V
 
EXPOSICION PYGAME
EXPOSICION PYGAMEEXPOSICION PYGAME
EXPOSICION PYGAME
 
Programación de Videojuegos con Python y Pilas (V)
Programación de Videojuegos con Python y Pilas (V)Programación de Videojuegos con Python y Pilas (V)
Programación de Videojuegos con Python y Pilas (V)
 
Desarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdf
Desarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdfDesarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdf
Desarrollo-de-Aplicaciones-Móviles-con-App-Inventor_ULS.pdf
 
Develop Mobile Apps with Corona SDK
Develop Mobile Apps with Corona SDKDevelop Mobile Apps with Corona SDK
Develop Mobile Apps with Corona SDK
 
Como instalar Python y librerias
Como instalar Python y libreriasComo instalar Python y librerias
Como instalar Python y librerias
 
Intro PyGame Capitulo 5
Intro PyGame Capitulo 5Intro PyGame Capitulo 5
Intro PyGame Capitulo 5
 
Codigo ejemplo j2 me
Codigo ejemplo   j2 meCodigo ejemplo   j2 me
Codigo ejemplo j2 me
 
Codemotion 2014 - Introducción a Unity
Codemotion 2014 - Introducción a UnityCodemotion 2014 - Introducción a Unity
Codemotion 2014 - Introducción a Unity
 

Más de Fernando Salamero

(Anotaciones) Ciencia (Cuestiones) que la tiza no propone
(Anotaciones) Ciencia (Cuestiones) que la tiza no propone(Anotaciones) Ciencia (Cuestiones) que la tiza no propone
(Anotaciones) Ciencia (Cuestiones) que la tiza no proponeFernando Salamero
 
Ciencia (Cuestiones) que la tiza no propone
Ciencia (Cuestiones) que la tiza no proponeCiencia (Cuestiones) que la tiza no propone
Ciencia (Cuestiones) que la tiza no proponeFernando Salamero
 
(Con anotaciones) En busca de la Física
(Con anotaciones) En busca de la Física(Con anotaciones) En busca de la Física
(Con anotaciones) En busca de la FísicaFernando Salamero
 
Timeline - En busca de la Física
Timeline - En busca de la FísicaTimeline - En busca de la Física
Timeline - En busca de la FísicaFernando Salamero
 
Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014
Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014
Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014Fernando Salamero
 
Programación de Videojuegos con Python y Pilas (IX)
Programación de Videojuegos con Python y Pilas (IX)Programación de Videojuegos con Python y Pilas (IX)
Programación de Videojuegos con Python y Pilas (IX)Fernando Salamero
 
Programación de Videojuegos con Python y Pilas (VIII)
Programación de Videojuegos con Python y Pilas (VIII)Programación de Videojuegos con Python y Pilas (VIII)
Programación de Videojuegos con Python y Pilas (VIII)Fernando Salamero
 
Programación con Pygame VII
Programación con Pygame VIIProgramación con Pygame VII
Programación con Pygame VIIFernando Salamero
 

Más de Fernando Salamero (18)

(Anotaciones) Ciencia (Cuestiones) que la tiza no propone
(Anotaciones) Ciencia (Cuestiones) que la tiza no propone(Anotaciones) Ciencia (Cuestiones) que la tiza no propone
(Anotaciones) Ciencia (Cuestiones) que la tiza no propone
 
Ciencia (Cuestiones) que la tiza no propone
Ciencia (Cuestiones) que la tiza no proponeCiencia (Cuestiones) que la tiza no propone
Ciencia (Cuestiones) que la tiza no propone
 
(Con anotaciones) En busca de la Física
(Con anotaciones) En busca de la Física(Con anotaciones) En busca de la Física
(Con anotaciones) En busca de la Física
 
Timeline - En busca de la Física
Timeline - En busca de la FísicaTimeline - En busca de la Física
Timeline - En busca de la Física
 
Jovenes físicos
Jovenes físicosJovenes físicos
Jovenes físicos
 
Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014
Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014
Taller de Pilas Engine, un motor de juegos en Python - PyConES 2014
 
Programación de Videojuegos con Python y Pilas (IX)
Programación de Videojuegos con Python y Pilas (IX)Programación de Videojuegos con Python y Pilas (IX)
Programación de Videojuegos con Python y Pilas (IX)
 
Programación de Videojuegos con Python y Pilas (VIII)
Programación de Videojuegos con Python y Pilas (VIII)Programación de Videojuegos con Python y Pilas (VIII)
Programación de Videojuegos con Python y Pilas (VIII)
 
Python básico II
Python básico IIPython básico II
Python básico II
 
Python básico I
Python básico IPython básico I
Python básico I
 
Python (ejercicios)
Python (ejercicios)Python (ejercicios)
Python (ejercicios)
 
Python (práctica 4)
Python (práctica 4)Python (práctica 4)
Python (práctica 4)
 
Python (práctica 3)
Python (práctica 3)Python (práctica 3)
Python (práctica 3)
 
Python (práctica 2)
Python (práctica 2)Python (práctica 2)
Python (práctica 2)
 
Python (práctica 1)
Python (práctica 1)Python (práctica 1)
Python (práctica 1)
 
Programación con Pygame VII
Programación con Pygame VIIProgramación con Pygame VII
Programación con Pygame VII
 
Aventura
AventuraAventura
Aventura
 
Programación con Pygame VI
Programación con Pygame VIProgramación con Pygame VI
Programación con Pygame VI
 

Programación con Pygame VIII

  • 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
  • 15. PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO cazador.py # -*- coding: utf-8 -*- import pygame from pygame.sprite import Sprite from pygame import * import util import sonidos # direcciones IZQUIERDA, 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() PÁGINA 15 DE 27 CC: FERNANDO SALAMERO
  • 16. PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO 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 PÁGINA 16 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
  • 25. PROGRAMA: MONKEY HUNTER CURSO: 1º BACHILLERATO 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 PÁGINA 25 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