Girando correctamente.

Hace mucho que no trabajamos en el juego, pero es que los examenes, las practicas, y el no tener ganas, hacen mucho daño. Hemos retomado el proyecto despues de un par de semanas, gracias a Edulix, que con su dark-extermination me ha motivado para que no abandone.

Así que sacando un poco de tiempo, que siempre hay, he arreglado un fallito que había cuando giraba el monigote. La cosa es que cuando giraba pegaba un salto extraño, y digo: “pues será que se cuela algún frame de por medio”. Pues sí, era eso:

En la función animate dentro de la clase AnimatedSprite, se comprueba que el frame por el que vamos no se sale de la animación:

 if self._frame >= len(self._animacion) + self._animacion[0]:
     self._frame = self._animacion[0]

Pero esto está mal, porque si self._frame == 0, no va a ser mayor, por lo que no se pone al primero de la animación, sino que se van pintando todos los frames desde el que estaba, hasta el primero de la animación.

Esto se puede arreglar comprobando que el frame está dentro de la animación:

 if not self._frame in self._animacion:
     self._frame = self._animacion[0]

Así ya no pasan cosas extrañas.

Otra cosa que he hecho es arreglar la carga de imagenes, cuando se pone con el flip. Porque para cargar la imagen invertida, giraba la original, y cargaba los frames de igual forma, cuando si hemos invertido la imagen original, tendremos que ir cargando frame a frame, pero invertido por columnas. Aquí está lo que he cambiado:

        if flip:
+            cols = range(columnas)
+            cols.reverse()
             for i in range(filas):
-                for j in range(columnas):
+                for j in cols:
                     aux_img = pygame.Surface((ancho, alto))
                     # el area para recortar
                     area = pygame.Rect(j*ancho, i*alto, ancho, alto)

Más cosas, en la clase Fighter he formalizado un poco el modo de indicar las animaciones. Ahora solo hay que indicar las animaciones en una dirección, para la otra dirección se modifican automáticamente.

        self.dir = 0
        self.parado = [0, 1, 2, 3, 4]
        self.andando = [5, 6, 7, 8, 9]
        self.saltando = [10,11,12]

self.dir indica la dirección, 0 imagenes por defecto, 1 invertido. Los otros tres indican los frames pertenecientes a las animaciones que hay por ahora.

Estas dos funciones son las que he implementado, para hacer un flip más o menos ordenado. Como las imagenes invertidas están a continuación de las originales, pues solo hay que sumarle el numero de frames iniciales a todas las animaciones, et voila!.

def change_dir(self, dir):
”’
cambia la orientacion del jugador
dir = 0 indica derecha
dir = 1 indica izquierda
”’
if self.dir == dir or not dir in [0,1]:
return False
else:
self.dir = dir
self.andando = map(self.masx, self.andando)
self.parado = map(self.masx, self.parado)
self.saltando = map(self.masx, self.saltando)
print self.andando, self.parado, self.saltando
return True

def masx(self, num):
”’
Sirve para cuando tenemos las imagenes invertidas,
si queremos los frames invertidos, tenemos que sumarle
el numero de imagenes a las animaciones
devuelve num + numimagenes si num < numimagenes num - numimagenes si num >= numimagenes
”’
if num < self.nimages: return num + self.nimages else: return num - self.nimages [/sourcecode]

Menu, Nuevo personaje y flip de imagen

Llevamos dos semanas en la que trabajamos poco en el proyecto, y es que yo estoy un poco liado, y el poco tiempo libre que tengo no me he puesto a trabajar en esto. Pero algo nuevo tenemos.

Weapp ha hecho una primera versión de menu principal, hay que ponerle imagenes de fondo, y cambiar los colores del menu, pero ya tenemos algo.

He dibujado un nuevo personaje, sólo tiene los frames de andar y saltar, y además no anda bien, hay que dibujar de nuevo esa animación.

tiete

Además de esto, hoy también he añadido la posibilidad de cargar también las imagenes invertidas, desde AnimatedSprite, así se pueden tener personajes que se den la vuelta.

  def load(self, path, filas, columnas, flip="True"):

He añadido un parametro opcional en la función load, flip, que cuando es True se cargan al final de todas las imagenes las imagenes invertidas.

    if flip:
            img_flip = pygame.transform.flip(img, True, False)
    .....
    if flip:
        for i in range(filas):
            for j in range(columnas):
                aux_img = pygame.Surface((ancho, alto))
                # el area para recortar
                area = pygame.Rect(j*ancho, i*alto, ancho, alto)
                aux_img.blit(img_flip, (0,0), area)
                aux_img = aux_img.convert()
                aux_img.set_colorkey(aux_img.get_at((0,0)))
                images.append(aux_img)
     .....

Lo he añadido en el ejemplo que tenemos, cambiando la animación normal por la animación invertida, pero hace una cosa extraña. Todavía queda pendiente una revisión de código, y una refactorización, para poder seguir avanzando de buena manera.

Efectos, zoom_out, zoom_in

Penyaskito comentó que haría falta algo para resaltar el tiempo, cuando queden menos de 5 segundos, y me vino a la mente ese efecto de zoomout, que no se donde he visto. La idea era que la imagen se fuera haciendo más grande y más transparente hasta desaparecer.

Pues bien, lo he implementado. Ha sido una cosa bastante simple, para ello he creado un nuevo módulo, llamado effect.py. Dentro de este módulo he creado la clase FX, que hereda de Sprite, y que va a mostrar una animación, una seríe de imágenes, hasta que sea la última, y en este caso desaparece, se destruye.

class FX(pygame.sprite.Sprite):
    def __init__(self, images, pos, fps=10):
        '''
        images: vector de imagenes a animar
        pos: posicion donde pintar el efecto
        fps: frames por segundo, velocidad del efecto
        '''
        pygame.sprite.Sprite.__init__(self)
        self._images = images
        self._animacion = range(len(self._images))

        self._start = pygame.time.get_ticks()
        self._delay = 1000 / fps
        self._last_update = 0
        self._frame = self._animacion[0]

        self.image = self._images[self._frame]
        self.rect = self.image.get_rect()
        self.pos = pos
        self.rect.center = self.pos
        
    def update(self):
        t = pygame.time.get_ticks()
        if t - self._last_update > self._delay:
            self._frame += 1
            if self._frame >= len(self._animacion) + self._animacion[0]:
                self.kill()
                return 0
            self.image = self._images[self._frame]
            self.rect = self.image.get_rect()
            self.rect.center = self.pos
            self._last_update = t

Los efectos los he implementado en en una función dentro del módulo effect, zoom_fx. Esta función recibe una imagen, y una posicion, y crea una animación de zoom a partir de esta.

def zoom_fx(surface, pos, mode="out",frames=5):
    '''
    Recibe una superficie, y una posicion, y devuelve una
    animacion de zoom out de esa imagen.
    mode puede ser "in" u "out".
    '''
    images = []
    images.append(surface)
    alpha = 255
    dec_alpha = alpha / frames
    for i in range(1, frames):
        aux_image = pygame.transform.rotozoom(images[i-1], 0, 1.1)
        aux_image = aux_image.convert()
        aux_image.set_colorkey(aux_image.get_at((0,0)))
        alpha = alpha - dec_alpha
        aux_image.set_alpha(alpha)
        images.append(aux_image)
    if mode == "in":
        images.reverse()
    animacion = FX(images, pos, 20)
    return animacion

La idea de esto es poder hacer tantos efectos como queramos, para hacer el juego más espectacular.

screenshot

Limpiando el código

Como el otro día nos pusimos a programar de aquella manera, ya tocaba limpiar un poco el código. Lo que he hecho hoy es crear una nueva clase Fighter, que representará a un jugador, y hasta ahora estaba incrustada, sin ser una clase con entidad propia, dentro de AnimatedSprite.

Por lo tanto he separado el viejo AnimatedSprite en dos. En teoría ahora AnimatedSprite es más generíco, y se puede usar para muchas más cosas. Esta clase se encarga solamente de cargar una imagen, y de animarla. Está orientada para ser una clase padre de sprites animados.

AnimatedSprite se ha quedado solo con los metodos:

  • def __init__(self, image, filas, columnas, fps = 10), el constructor
  • def animate(self, t), el método animate, que cambia de frame segun el tiempo y fps. Se debería llamar cada vez que se llama a update de la clase hija y se quiere animar
  • def load(self, path, filas, columnas), carga la imagen, y crea el vector self._images, recortando la imagen original en frames segun las filas y las columnas
  • def set_frame(self, frame), pone un frame determinado de los posibles, self._images
  • def set_animation(self, animation), pone una animación, como la actual

Así pues he creado una nueva clase Fighter, que hereda de AnimatedSprite, y que es un luchador en concreto, con una velocidad, unas animaciones, etc. Quizás más adelante tengamos que crear clases que hereden de Fighter, puesto que queremos distintas clases de jugadores, con distintos movimientos, por lo que creo que esto sería la mejor opción.

De momento la clase Fighter es provisional, y tiene algunas propiedades del luchador que tenemos en concreto. Cuando tengamos más de un luchador habrá que separa en varias clases, y creo que la herencia sería la mejor opción.

# $Id$

import pygame
import AnimatedSprite

class Fighter(AnimatedSprite.AnimatedSprite):
    def __init__(self, image, filas, columnas, fps=10):
        AnimatedSprite.AnimatedSprite.__init__(self, image, filas, columnas, fps)
        # animaciones propias
        self.parado = [0, 1, 2, 3, 4]
        self.andando = [5, 6, 7, 8, 9]
        # la posicion en pantalla
        self.center = [100,100]
        self.rect.center = self.center
        # flags de estados
        # TODO hacer esto con binarios
        self.andando_derecha = False
        self.andando_izquierda = False
        # velocidad de movimiento horizontal
        self.velocidad = 5
        # velocidad de movimiento vertical
        self.vel_caida = 0
        # aceleracion de vel_caida
        self.gravedad = 0.7
        # posicion del suelo, en pixels
        self.suelo = 460

    def update(self):
        self.set_animation(self.parado)
        if self.andando_derecha or self.andando_izquierda:
            self.set_animation(self.andando)

        # Callendo
        if not self.en_suelo() or self.vel_caida &lt; 0:
            if self.vel_caida  10:
                self.set_frame(12)
            else: self.set_frame(11)
            self.vel_caida += self.gravedad
            self.center[1] += self.vel_caida
        else: self.vel_caida = 0
        ###
        if self.en_suelo():
            self.animate(pygame.time.get_ticks())
        self.rect = self.image.get_rect()
        self.rect.center = self.center
            
        if self.andando_derecha:
            self.center[0] += self.velocidad
        elif self.andando_izquierda:
            self.center[0] -= self.velocidad


    def en_suelo(self):
        return self.rect.bottom &gt;= self.suelo

Timer

Como indicaba Dani en la entrada anterior, la clase Timer no funcionaba como es debido tras la tarde del martes. Esa noche la terminé, utilizando para ello las librerías de python en vez de las de pygame.

Además, añadí la posibilidad de parar y reanudar el reloj, para que en el futuro se pueda implementar la pausa.

Es un código bastante sencillo, aquí lo tenéis:

# $Id: Timer.py 14 2007-11-01 21:07:45Z penyaskito $

import pygame
import time
import ResourceLoader

class Timer(pygame.sprite.Sprite):

    TIME = 60
    
    ESTADO_PARADO = 0
    ESTADO_CORRIENDO = 1
    
    def __init__(self, screen):
        self._posicion = (screen.get_width() / 2 - 20, 10)
        self._screen = screen
        self._inicial = time.time()
        self._estado = self.ESTADO_CORRIENDO
        self._seconds = self.TIME
        self._font = pygame.font.SysFont("Verdana",40)
        
    def update(self, t): 
        s = self._font.render(str("%(var)02d" % {"var":self._seconds}),True,(255,230,50))

        self._screen.blit (s, self._posicion)        
        if self._estado == self.ESTADO_CORRIENDO:
            self._seconds = self.TIME - (time.time() - self._inicial)
        if self._seconds &lt; 0:
            self._seconds = 0
            self._estado = self.ESTADO_PARADO
    
    def get_seconds(self):
        return self._seconds
        
    def pause(self):
        self._estado = self.ESTADO_PARADO
        # almacenamos el instante en el que fue parado.
        self._stopped = time.time()
        
    def resume(self):
        # calcula por donde debe seguir la cuenta.
        self._inicial -= self._stopped - time.time()
        self._estado = self.ESTADO_CORRIENDO

No me acaba de convencer que la clase responsable de llevar el tiempo sea la responsable de pintarlo, pero como decía Dani, ya iremos refinando en el futuro. Recuerda: Commit early. Commit often.

Próximamente, veremos como usar un sistema de eventos en python, de manera que se pueda reducir el acoplamiento entre algunas clases.

Monigote animado y marcador

Hoy ha sido un día productivo. Hemos avanzado bastante, y aunque el código no esté muy limpio, la cosa funciona, ya limpiaremos el código. Voy a comentar un poco lo que hemos conseguido.

Primer Paso: Cargar un monigote en forma de tiles.

Teniamos algunos monigotes que trajo Nasek, estuve mirando el Ryu, pero no estaba bien organizada la imagen, no tenían el mismo ancho cada frame, ni estaban encuadrados bien. Así que después de intentar recortar al Ryu hemos cogido la imagen de losersjuegos. Y como esta estaba bien, pues me he puesto a programar.

He cogido de base la clase AnimatedSprite de este tutorial, ya que queremos que el monigote se mueva mientras está parado, y cuando anda.
Para cargar una imagen, lo que hago es usar el ResourceLoader para cargar la imagen completa, con todos los tiles, y le paso el numero de imagenes que hay por columna, y por fila, para así dividir el ancho total de la imagen por el numero de columnas, y obtener el ancho de cada imagen, e igualmente el alto. Una vez conseguido esto creo un vector de imagenes donde voy metiendo cada frame recortado de la imagen:


 81   def load(self, path, filas, columnas):
 82         """
 83         Carga una imagen de tipo tile, en un vector de imagenes.
 84         Devuelve un vector de imagenes.
 85         path es la ruta a la imagen
 86         filas es el numero de frames en una columna
 87         columnas es el numero de frames en una fila
 88         """
 89         images = []
 90         img = ResourceLoader.ResourceLoader.load(path)
 91         img = pygame.transform.rotozoom(img, 0, 3)
 92         alto = img.get_height() / filas
 93         ancho = img.get_width() / columnas
 94         aux_img = pygame.Surface((ancho, alto))
 95 
 96         for i in range(filas):
 97             for j in range(columnas):
 98                 aux_img = pygame.Surface((ancho, alto))
 99                 area = pygame.Rect(j*ancho, i*alto, ancho, alto)
100                 aux_img.blit(img, (0,0), area)
101                 aux_img = aux_img.convert()
102                 aux_img.set_colorkey(aux_img.get_at((0,0)))
103                 images.append(aux_img)
104 
105         return images

Con esto ya tenemos todas las imagenes en un vector, y podemos acceder a cada una de ellas con self._images[frame].

Para cuando anda he modificado el update de AnimatedSprite, pudiendo cambiar la animación. Una animación es una lista de frames a utilizar. También le he metido una gravedad y un suelo, para que caiga, y poder saltar. Para saltar lo único que hago es poner la variable vel_caida negativa, y en cada frame le voy sumando la gravedad, por lo que sube y baja de forma suave, con aceleración.


 48         if not self.en_suelo() or self.vel_caida &lt; 0:
 49             if self.vel_caida  10:
 52                 self.image = self._images[12]
 53             else: self.image = self._images[11]
 54             self.vel_caida += self.gravedad
 55             self.center[1] += self.vel_caida
 56         else: self.vel_caida = 0

Se coloca una imagen dependiendo de si está subiendo, bajando, o en la transición entre subida y bajada.

Con respecto al marcador, ha estado trabajando en él Penyaskito. Ha hecho unas barras de vida, que se decrementan de forma animada. Para ello ha pintado un rectangulo de fondo, y uno sobre este que es la barra de vida. De momento está puesto de tal forma que si pulsas espacio se decrementa. En principio tiene 100 de vida, y cuando pulsas espacio va bajando la vida hasta un limite impuesto. De esta forma se consigue que esté animado.

También se ha creado un reloj, pero aún no funciona bien, así que hay que mirarlo, y utilizar las funciones de tiempo de python, import time, y calcular el tiempo con eso.

Y creo que eso es todo, Penyaskito, cuenta tú algo más detallado de lo que has hecho en comentarios, si te parece. Yo ya no escribo más. Eso sí, voy a poner un screenshot.

Accesos a disco: ResourceLoader

Es mi primera entrada en este blog de desarrollo, así que lo primero que debería hacer es presentarme. Soy Penyaskito, y junto a Danigm y otros colegas de Sugus vamos a desarrollar este juego. Es una experiencia nueva, y espero que sea muy divertida y didáctica. Al contrario que mis compañeros, yo usaré Windows para el desarrollo, de manera que la compatibilidad estará garantizada.

Nunca había usado pygame antes, así que uno de los primeros pasos es aprender como funciona esta plataforma. Para ello, Dani preparó un taller en el que realizó un jueguecito de naves, que puedes descargar aquí: Shooter2D.

Una de las cosas que me llamó la atención al revisar el código era que cada objeto de la clase Nave cargaba sus propias imágenes. Pensé que quizá pygame se encargaría de controlar y gestionar los accesos a disco, así que decidí comprobarlo usando FileMon. Gracias a esta herramienta descubrí que no era así:

filemon-config.png

Configuración de la captura de FileMon

pygame-load.png

Captura del juego corriendo, con FileMon registrando los accesos al fichero de imagen de la nave enemiga.

Visto que no, es necesaria una clase que se encargue de gestionar los accesos a disco, de modo que los recursos que se accedan repetidamente sean cacheados en memoria. Para ello, desarrollé esta sencilla clase ResourceLoader:

import pygame
import sys

class ResourceLoader():
    '''
    Clase encargada de gestionar la carga de imagenes. 
    Cachea estas si es necesario.
    '''
    images = {}
        
    def load (self, uri, cache=False):
        '''
        Carga una imagen (uri). Permite el cacheo si cache=True.
        '''
        img = None
        if uri in self.images:
            img = ResourceLoader.images[uri]
        else:
            img = pygame.image.load(uri)
            if cache:
                ResourceLoader.images[uri] = img
        return img

A esta clase aún le falta un correcto manejo de excepciones, así como comprobaciones de que la uri pasada sea válida y se pueda utilizar como clave de un diccionario.

Tras adaptar la clase Nave para que utilicé el ResourceLoader, al lanzar el juego con FileMon obtenemos la siguiente captura:
resourceloader-load.png

Captura de FileMon registrando los accesos al fichero de imagen de la nave enemiga tras los cambios.

En la captura podemos ver que los accesos a disco se han reducido, ya que sólo se realizan la primera vez.

Esto ha sido todo por hoy. Pronto trabajaremos en las clases principales del juego, y tendremos a luchadores sudorosos brincando por la pantalla. ¡Hasta pronto!

Herramientas para el desarrollo

Hemos hecho algunos cambios en las herramientas que vamos a usar. Lo primero es que vamos a cambiar el código de launchpad a googlecode, vamos a utilizar subversion, y la forja de google, que es más sencilla.

suburban-fighters.googlecode.com

El blog lo he cambiado, porque me había equivocado en el nombre, y puse figthers en lugar de fighters.

suburbanfighters.wordpress.com

El grupo de google, sigue igual, se ha integrado todo dentro de googlecode, de forma que desde ahí se puede acceder a todo. También se ha creado una página especial en el wiki de sugus, http://sugus.eii.us.es/wiki/Proyectos/Taller_de_videojuegos/Suburban-Fighters

Sobre el desarrollo

Hoy después de la charla de subversion, hemos estado mucha gente en sugus, hablando sobre este proyecto, y nos hemos ido por los cerros de Ubeda, que si esquema de estados para los combos del jugador, que si colisiones eficientes….

Hay varias formas de realizar un proyecto, pero el objetivo de este juego es enseñar, y hacer la programación más divertida, por lo tanto este juego debe ser simple, por lo menos en principio, y luego ir avanzando según se vayan adquiriendo más conocimientos. Así pues seguimos con esa idea, y el primer objetivo es tener un monigote pintado en panatalla que ande y salte.

Después de comentar esto, vamos a lo importante.

He creado el blog como medio de coordinación, y difusión del proyecto, que la gente de fuera pueda ver lo que hacemos, así como que nosotros comentemos qué estamos haciendo y qué pensamos en este blog. Así pues, os añadiré a todos los que participéis aquí, para que podáis postear.

Para entendernos, vamos a proponer, o a llevar un control de lo que hacemos, o lo que queremos hacer durante la semana; así pues, voy a proponer los objetivos de esta semana:

  • Necesitamos gráficos, y cómo por ahora no los tenemos, pues vamos a coger prestados algunos del street fighters. Yo voy a ver si hago algún fondito, y algún diseño de personajes a lapiz, para que el martes que viene, entre todos decidamos algo.
  • También necesitamos una historia, en un juego de lucha, pues parece algo extraño, pero hay que inventarse algo, así que alguien vaya pensando algo, tampoco corre prisa esto.

Herramientas que vamos a usar:

Así pues, el que quiera participar, que se registre en wordpress.com, si quiere poder escribir en el blog, y que se registre en launchpad, si quiere poder subir código, reportar bugs y demás.

Los días de reunión serán los martes, discutiremos sobre lo que hemos hecho, lo siguiente a hacer, y programaremos algo. El martes que viene voy a dar la introducción a python con pygame, así que algo veremos, lo haré rápido, y prepararé algo, para que no sea tan caótico como hoy.