top of page
Buscar
  • Foto del escritorFernando Sansberro

12. Animación de Sprites

Actualizado: 24 dic 2021

En este capítulo veremos cómo hacer animaciones de sprites. Las animaciones son parte clave de los videojuegos, y hacen que los elementos del juego cobren vida. Para animar un sprite, necesitamos tener varias imágenes, que iremos mostrando sucesivamente para crear la ilusión de movimiento (animación).


Los Cuadros de una Animación


Hasta ahora nuestros sprites tienen solamente una imagen, pero un sprite puede tener más de una imagen si se la cambiamos cada cierto tiempo, para crear una animación

Comenzaremos haciendo que las naves enemigas tengan animación en lugar de tener una única imagen fija. Para esto, necesitamos tener una secuencia de imágenes correspondientes a la animación.



Figura 12-1: Cuadros (frames) de la nave enemiga.



A cada una de las imágenes pertenecientes a la animación, se le denomina cuadro o frame de animación. En el caso de la nave enemiga, como se muestra en la figura 12-1, la animación tiene nueve cuadros.


Estas imágenes deben estar nombradas secuencialmente con un número al final, por ejemplo, grey_ufo_00.png, grey_ufo_01.png, grey_ufo_02.png hasta grey_ufo_08.png, de forma tal que sea más sencillo cargar la secuencia de imágenes en forma consecutiva desde la programación, como veremos a continuación.


Un sprite animado podrá tener muchas imágenes en su animación, pero solo una será la que se muestra en el frame actual. Luego, la imagen se va cambiando a medida que pasan los frames, haciendo que se vayan mostrando una a una toda la secuencia de imágenes de la animación.


La Clase CAnimatedSprite


Para implementar un sprite con animación, haremos una clase denominada CAnimatedSprite que heredará de CSprite. Cuando necesitemos un sprite animado, haremos una clase (como por ejemplo la clase CNave) que heredará de CAnimatedSprite y ésta tendrá funciones relacionadas a la animación, heredando todo lo que ya tenemos de la clase CSprite. El diseño de estas dos clases queda de la siguiente manera:



Figura 12-2: Diagrama de clases con la clase CAnimatedSprite.



Como vemos en el diagrama, la clase CSprite contiene la imagen que se muestra en la pantalla, y la clase CAnimatedSprite la cambia cada cierto tiempo para hacer la animación.

Antes de comenzar a implementar la clase CAnimatedSprite, debemos preparar a la clase CSprite para poder cambiar su imagen.


La Función setImage() de la Clase CSprite


En el último ejemplo que realizamos, la clase CSprite está cargando la imagen del sprite en la función init(). Esto ahora ya no nos sirve, porque esta imagen va a cambiar continuamente si el sprite es animado. Lo que haremos es que cada clase (cada objeto del juego) cargue las imágenes que vaya a utilizar y simplemente se indique cual es la imagen que se va a dibujar, utilizando una función denominada setImage(), que haremos a continuación, y que de esta forma no se vuelva a cargar una imagen desde el archivo, sino que simplemente la cambiamos.


Veamos el ejemplo de la carpeta capitulo_12\001_set_image_clase_sprite. Este ejemplo hace lo mismo que el último ejemplo que realizamos, pero se ha modificado la clase CSprite para que ya no reciba una imagen como parámetro y cargarla, sino que cargar la imagen sea responsabilidad de la clase superior. Se agrega la función setImage() en la clase CSprite, para cargar una imagen. El código del constructor de CSprite y la función setImage() quedan así:


...

class CSprite(CGameObject):

   # Constructor:
   def __init__(self):

       CGameObject.__init__(self)
      
       # Imagen (superficie) del sprite.
       # Usar setImage() en la clase superior para establecer una imagen.
       self.mImg = None

       # Ancho y alto de la imagen. La función setImage() los actualiza.
       self.mWidth = 0
       self.mHeight = 0

   # Establecer la imagen del sprite.
   def setImage(self, aImg):

       # Establecer la imagen.
       self.mImg = aImg

       # Guardar el ancho y el alto.
       self.mWidth = self.mImg.get_width()
       self.mHeight = self.mImg.get_height()

...

Nota que el constructor de CSprite ya no recibe la imágen como parámetro.

Cuando hagamos un objeto del juego (como por ejemplo las clases CEnemyBullet, CPlayerBullet, CNave y CPlayer), haremos una clase que heredará de CSprite, cargando la imagen que corresponda y estableciéndola usando la función setImage() en CSprite. La función setImage() además, establece el ancho y el alto de la imagen, lo cual se utiliza en las colisiones.


Las clases CEnemyBullet, CPlayerBullet, CNave y CPlayer ahora modifican su constructor, para cargar la imagen y establecerla con setImage(). Estas clases quedan como se muestra a continuación.


...

class CEnemyBullet(CSprite):

   # Constructor.
   def __init__(self):
       CSprite.__init__(self)

       img = pygame.image.load("assets/images/enemy_bullet.png")
       img = img.convert_alpha()
       self.setImage(img)

...


...

# La clase CPlayerBullet hereda de CSprite.
class CPlayerBullet(CSprite):

   # Constructor.
   def __init__(self):
       CSprite.__init__(self)

       img = pygame.image.load("assets/images/player_bullet.png")
       img = img.convert_alpha()
       self.setImage(img)

...


...

# La clase CPlayer hereda de CSprite.
class CPlayer(CSprite):

   ...

   def __init__(self, aType):
       CSprite.__init__(self)

       # Segun el tipo de la nave, la imagen que se carga.
       self.mType = aType
       if self.mType == CPlayer.TYPE_PLAYER_1:
           imgFile = "assets/images/player00.png"
       elif self.mType == CPlayer.TYPE_PLAYER_2:
           imgFile = "assets/images/player10.png"

       img = pygame.image.load(imgFile)
       img = img.convert_alpha()
       self.setImage(img)

...


...


# La clase CNave hereda de CSprite.
class CNave(CSprite):

    ...

    def __init__(self, aType):
        CSprite.__init__(self)

        # Segun el tipo de la nave, la imagen que se carga.
        self.mType = aType
        if self.mType == CNave.TYPE_PLATINUM:
            imgFile = "assets/images/grey_ufo.png"
        elif self.mType == CNave.TYPE_GOLD:
            imgFile = "assets/images/yellow_ufo.png"
        elif self.mType == CNave.TYPE_RED:
            imgFile = "assets/images/red_ufo.png"
        elif self.mType == CNave.TYPE_GREEN:
            imgFile = "assets/images/green_ufo.png"
        elif self.mType == CNave.TYPE_CYAN:
            imgFile = "assets/images/cyan_ufo.png"

        img = pygame.image.load(imgFile)
        img = img.convert_alpha()
        self.setImage(img)

...

Como ahora cada clase del juego es responsable de cargar la imagen y establecerla con setImage() en el sprite, puede ocurrir que nos olvidemos de establecerla. Entonces, en la clase CSprite ponemos un chequeo al dibujar por si nos olvidamos de establecer la imagen, para evitar que de error al querer dibujar una imagen que tiene el valor None (esto ocurre cuando no se ha inicializado la variable). La función render() de CSprite queda así:


# Dibuja el objeto en la pantalla.
# Parámetros: La superficie de la pantalla donde dibujar la imagen.
def render(self, aScreen):

   if (self.mImg != None):
       aScreen.blit(self.mImg, (self.getX(), self.getY()))

De esta manera, si en una clase que hereda de CSprite, nos olvidamos de establecer una imagen, no dará error, aunque la imagen no se verá en la pantalla. Recordar entonces que al crear un sprite, hay que cargar la imagen y usar setImage() para establecer la imagen cargada.


Animando un Sprite


Con el código de CSprite que simplemente maneja una imagen y que se puede cambiar con la función setImage(), es simple escribir una clase por encima para que el sprite sea animado. Esta clase se llamará CAnimatedSprite y heredará de CSprite, la cual ya maneja el dibujo de una imagen y su colisión.


Veamos el ejemplo ubicado en la carpeta capitulo_12\002_animacion_de_sprites. En este ejemplo, las naves enemigas se encuentran animadas. Cuesta verlo al inicio, porque la animación es muy rápida. Veamos a continuación los pasos necesarios para crear nuestro primer sprite animado.


Cargando las Imágenes


Las naves enemigas (mira el código en la clase CNave), en lugar de utilizar una sola imagen estática como estábamos haciendo hasta ahora, vamos a usar una secuencia de imágenes. Mira en el proyecto la carpeta assets\images, y verás que hemos puesto los nueve cuadros de animación para cada una de las naves. Debes copiar todas estas imágenes a tu propio proyecto.



Figura 12-3: Las imágenes de los frames de las naves animadas.



Los archivos los hemos nombrado de la siguiente manera: <color>_ufo_01 a <color>_ufo_08. Donde dice <color>, significa que en esa parte del nombre va el color correspondiente al tipo de nave. Ahora tenemos cinco tipos de naves enemigas y cada nave tiene nueve frames de animación.

Según el tipo de nave, ahora deberemos cargar la secuencia de imágenes correspondiente. Observa cómo se cargan las imágenes en el constructor de la clase CNave. La clase se lista a continuación.


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

# -------------------------------------------------------------------
# Clase CNave.
# Nave enemiga que se mueve por la pantalla chequeando los bordes.
#
# Autor: Fernando Sansberro - Batovi Games.
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
# -------------------------------------------------------------------

# Importar Pygame.
import pygame

# Importar la clase para sprites animados.
from api.CAnimatedSprite import *

# Importar las clases necesarias para disparar.
import random
from game.CEnemyBullet import *
from api.CGameConstants import *
from api.CEnemyManager import *

# La clase CNave hereda de CAnimatedSprite.
class CNave(CAnimatedSprite):

   # Tipos de naves.
   TYPE_PLATINUM = 0
   TYPE_GOLD = 1
   TYPE_RED = 2
   TYPE_GREEN = 3
   TYPE_CYAN = 4

   # ----------------------------------------------------------------
   # Constructor. Recibe el tipo de nave (TYPE_PLATINUM o TYPE_GOLD).
   # ----------------------------------------------------------------
   def __init__(self, aType):
       CAnimatedSprite.__init__(self)

       # Segun el tipo de la nave, la imagen que se carga.
       self.mType = aType

       if self.mType == CNave.TYPE_PLATINUM:
           imgFile = "assets/images/grey_ufo_0"
       elif self.mType == CNave.TYPE_GOLD:
           imgFile = "assets/images/yellow_ufo_0"
       elif self.mType == CNave.TYPE_RED:
           imgFile = "assets/images/red_ufo_0"
       elif self.mType == CNave.TYPE_CYAN:
           imgFile = "assets/images/cyan_ufo_0"
       elif self.mType == CNave.TYPE_GREEN:
           imgFile = "assets/images/green_ufo_0"

       # Cargar la secuencia de imágenes.
       self.mFrames = []
       i = 0
       while (i <= 8):
           tmpImg = pygame.image.load(imgFile + str(i) + ".png")
           tmpImg = tmpImg.convert_alpha()
           self.mFrames.append(tmpImg)
           i = i + 1

       # Inicializar la animación.
       # Se pasa el array de imágenes y el primer frame.
       self.initAnimation(self.mFrames, 0)

   # Mover el objeto.
   def update(self):

       # Invocar update() de CAnimatedSprite.
       CAnimatedSprite.update(self)

       # Ver si la nave dispara.
       if random.randrange(1, 500) == 1:
           b = CEnemyBullet()
           b.setXY(self.getX() + self.getWidth() / 2 - b.getWidth() / 2, self.getY() + self.getHeight())
           b.setVelX(0)
           b.setVelY(10)
           b.setBounds(0, 0, CGameConstants.SCREEN_WIDTH, CGameConstants.SCREEN_HEIGHT)
           b.setBoundAction(CGameObject.DIE)
           CEnemyManager.inst().add(b)

   # Dibuja el objeto en la pantalla.
   # Parámetros:
   # aScreen: La superficie de la pantalla en donde dibujar.
   def render(self, aScreen):

       # Invocar render() de CAnimatedSprite.
       CAnimatedSprite.render(self, aScreen)

   # Liberar lo que haya creado el objeto.
   def destroy(self):

       # Invocar destroy() de CAnimatedSprite.
       CAnimatedSprite.destroy(self)

       # Eliminar todos los frames creados.
       i = len(self.mFrame)
       while i > 0:
           self.mFrame[i - 1] = None
           self.mFrame.pop(i - 1)
           i = i - 1
       # Eliminar el array.
       self.mFrames = None

La clase CNave ahora hereda de la clase CAnimatedSprite. En la función init(), cargamos una secuencia de imágenes que las guardaremos en un array llamado self.mFrames.


Al inicio, según el tipo de nave, se crea un string con la primera parte del nombre. Luego se crea el array que va a tener las imágenes de la animación. Se crea el array self.mFrames como un array vacío (determinado por los dos corchetes: []) .


Luego, se recorre desde cero hasta el último número de imagen que corresponda a la animación. En el caso de la nave gris, los nombres van desde "grey_ufo_00.png" a "grey_ufo_08.png", siendo en total nueve cuadros para esta animación de la nave.


En cada paso del loop, se carga la imagen correspondiente al número de frame actual (la variable i) en una variable temporal tmpImg y como siempre, se convierte al formato de Pygame. Por último se agrega al array de imágenes usando la función append().


Nota que para armar el nombre del archivo de imagen a cargar se usa una concatenación de strings, para colocar el número entre el nombre y la extensión del archivo (".png").


Al final, llamamos a la función initAnimation() de CAnimatedSprite, que lo que hace es establecer el array de frames, decir cual es el frame actual y cargar la primera imagen a mostrar.


A continuación veremos la clase CAnimatedSprite, pero para terminar con la clase CNave, es necesario notar que ahora en las funciones update(), render() y destroy(), se invocan a las respectivas funciones de CAnimatedSprite (antes era CSprite porque la clase CNave heredaba de CSprite, pero ahora hereda de CAnimatedSprite). Al final, en la función destroy(), se liberan de memoria las imágenes cargadas y see eliminará el array de frames.


Si repasamos la clase CNave, vemos que solamente carga las imágenes y luego invoca a initAnimation() (de la clase CAnimatedSprite). Luego, como update() y render() invocan a las respectivas funciones en CAnimatedSprite, vemos que el código de la animación se encuentra en CAnimatedSprite. Veamos esta clase a continuación.


Inicializando la Animación


La clase CAnimatedSprite contiene una referencia al array con las imágenes de la animación (self.mFrame) y un atributo self.mCurrentFrame que lleva control del cuadro actual que hay que dibujar. Veamos la clase CAnimatedSprite completa:


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

#--------------------------------------------------------------------
# Clase CAnimatedSprite.
# Sprite con animación. Hereda de Sprite.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------

import pygame

# Importar la clase CSprite.
from api.CSprite import *

class CAnimatedSprite(CSprite):

   def __init__(self):

       CSprite.__init__(self)

       # Array con los frames de la animación (las imágenes).
       self.mFrame = None

       # Frame actual de la animación.
       self.mCurrentFrame = 0

   # Inicializa la animación del sprite. Establece el array de imágenes
   # de la animación y el frame actual.
   def initAnimation(self, aFramesArray, aStartFrame):
       self.mFrame = aFramesArray
       self.mCurrentFrame = aStartFrame
       self.setImage(self.mFrame[self.mCurrentFrame])
      
   def update(self):
      
       CSprite.update(self)

       # Actualizar en cuadro de animación.
       self.mCurrentFrame = self.mCurrentFrame + 1
       if self.mCurrentFrame >= len(self.mFrame):
           self.mCurrentFrame = 0
       self.setImage(self.mFrame[self.mCurrentFrame])

   def render(self, aScreen):
      
       CSprite.render(self, aScreen)

   def destroy(self):
      
       CSprite.destroy(self)
      
       i = len(self.mFrame)
       while i > 0:
           self.mFrame[i-1] = None
           self.mFrame.pop(i-1)
           i = i - 1

En el constructor de CNave, luego de cargar la secuencia de imágenes, debemos invocar a la función initAnimation(), pasándole como parámetro el array de imágenes y el frame inicial a mostrar. Si no hacemos esto, no aparecerá nada en la pantalla (recuerda que no dará error si no inicializamos la animación porque pusimos un control en la función render() de la clase CSprite, para que no intente dibujar una imagen que no ha sido inicializada, aunque no veremos nada en pantalla si nos olvidamos de inicializar la animación en la clase CNave, o en general en cualquier clase que herede de CAnimatedSprite).


La función initAnimation() guarda el array de imágenes de la animación y el frame actual, y llama a setImage() en CSprite para establecer la primera imagen a utilizar (recuerda que esta función establece el ancho y el alto de la imagen, que luego se usa en las colisiones).


Actualizando la Animación en update()


La función update() de CAnimatedSprite tiene código para actualizar el frame actual de la animación, incrementándose en uno. Cuando el frame actual llega a la cantidad de frames totales, se vuelve a cero para que la animación comience nuevamente desde el principio. Recuerda que si se accede a un array fuera de rango, aparecerá un error y no queremos que eso ocurra.


def update(self):
  
   CSprite.update(self)

   # Actualizar en cuadro de animación.
   self.mCurrentFrame = self.mCurrentFrame + 1
   if self.mCurrentFrame >= len(self.mFrame):
       self.mCurrentFrame = 0
   self.setImage(self.mFrame[self.mCurrentFrame])

Cuando se cambia de frame, se invoca a la función setImage() para cambiar de imagen. Más adelante haremos otros tipos de animaciones que no sean cíclicas como esta. Por ahora las animaciones serán todas cíclicas (denominada animación en loop).


Por último, en la función render(), en lugar de mostrar en pantalla siempre la misma imagen como se hacía con sprites estáticos, se muestra la imagen correspondiente al frame actual (esto va cambiando la imagen que se muestra en pantalla y avanzando en la secuencia de frames).


Al ejecutar el juego, vemos que la animación de la nave cambia de cuadro en cada frame, y por eso no luce del todo bien (los cuadros cambian muy rápido). Por ahora no estamos controlando el frame rate de la animación, y la animación ocurre muy rápido. A continuación veremos cómo controlar la velocidad de la animación.


Controlando la Velocidad de la Animación del Sprite


En el ejemplo anterior, en cada frame del juego se está cambiando el cuadro de la animación. Como el juego corre a 60 frames por segundo, eso es muy rápido para que la mayoría de las animaciones se vean bien. Lo que haremos es establecer una demora (denominado delay) entre cuadro y cuadro de animación.


Veamos el ejemplo ubicado en la carpeta capitulo_12\003_animacion_controlada. En este ejemplo, agregamos a la clase CAnimatedSprite el código para tener una animación controlada. Si ejecutamos el juego vemos que las naves enemigas tienen más lenta la animación.


En la clase CNave, inicializamos la animación pasando un tercer parámetro que indica el delay de la animación. En este caso le pasamos 8, que indica que va a estar ocho cuadros con la misma imagen, antes de cambiar de cuadro. Veamos este cambio de la clase CNave:


...

# La clase CNave hereda de CAnimatedSprite.
class CNave(CAnimatedSprite):

    ...

    def __init__(self, aType):

        CAnimatedSprite.__init__(self)
        
        ...
        
   # Inicializar la animación.
   # Se pasa el array de imágenes y el primer frame.
   self.initAnimation(self.mFrames, 0, 8)

...

Lo que haremos entonces es enlentecer la animación. Para esto, en lugar de cambiar de cuadro en cada frame, lo haremos esperar que pasen una cierta cantidad de frames del juego (ocho en este caso) antes de cambiar de frame en la animación.


Para hacer esto, programaremos este comportamiento en la clase CAnimatedSprite. Veamos esta clase.


...

class CAnimatedSprite(CSprite):

   def __init__(self):

       CSprite.__init__(self)

       # Array con los frames de la animación (las imágenes).
       self.mFrame = None

       # Frame actual de la animación.
       self.mCurrentFrame = 0

       # Control de cuando pasar de frame.
       self.mTimeFrame = 0
       # Cuantos frames deben pasar antes de pasar de imagen.
       self.mDelay = 0

   # Inicializa la animación del sprite. Establece el array de imágenes
   # de la animación, el frame actual y el delay de la animación.
   def initAnimation(self, aFramesArray, aStartFrame, aDelay):
       self.mFrame = aFramesArray
       self.mCurrentFrame = aStartFrame
       self.mTimeFrame = 0
       self.mDelay = aDelay
       self.setImage(self.mFrame[self.mCurrentFrame])
      
   def update(self):
      
       CSprite.update(self)

       # Ver si hay que cambiar de frame.
       self.mTimeFrame = self.mTimeFrame + 1
       if (self.mTimeFrame > self.mDelay):
           # Resetear el tiempo.
           self.mTimeFrame = 0

           # Actualizar en cuadro de animación.
           self.mCurrentFrame = self.mCurrentFrame + 1
           if self.mCurrentFrame >= len(self.mFrame):
               self.mCurrentFrame = 0
           self.setImage(self.mFrame[self.mCurrentFrame])

...

Se han agregado dos variables en la clase CAnimatedSprite. La variable self.mTimeFrame comienza en cero y lleva el control del tiempo (la cantidad de frames) que la animación se encuentra en el cuadro actual. La variable self.mDelay contiene la cantidad de tiempo (frames) que deben pasar antes de avanzar al siguiente cuadro de animación.


Como se ve en el código de update(), lo que hemos agregado es código que le suma uno a self.mTimeFrame y no cambia de cuadro hasta que este valor alcance el indicado por self.mDelay. Cuando esto ocurre, se avanza de frame y se vuelve a poner self.mTimeFrame en cero para comenzar a contar el delay desde cero para el siguiente cuadro.


Siendo que el juego está corriendo a 60 frames por segundo, los delays que podemos usar son: 0 = 60 FPS, 1 = 30 FPS, 2 = 20 FPS, 3 = 15 FPS, 4 = 12 FPS, … 59 = 1 FPS.

Prueba a cambiar el parámetro del delay en la clase CNave, para ver cómo queda la velocidad de animación. Si ponemos 59 de delay, veremos que la animación cambia de a un cuadro por segundo.


Animando la Nave del Jugador


Lo que haremos ahora es animar a la nave del jugador. La animación será diferente a lo que hemos hecho con la nave enemiga, debido a que esta nave usará otro tipo de animación. La animación de la nave enemiga era cíclica porque es un plato volador girando (un loop de animación), mientras que la animación de la nave del jugador tiene un cuadro para cuando no se mueve, un cuadro para cuando se mueve a la derecha y un cuadro para cuando se mueve a la izquierda. En la siguiente imagen se muestran los cuadros de la nave del jugador.



Figura 12-4: Cuadros de la animación de la nave del jugador.



Veamos el ejemplo ubicado en la carpeta carpeta capitulo_12\004_animacion_jugador. Como siempre, debes copiar las imágenes a utilizar, a tu propio proyecto.


En la función init() de la clase CPlayer, cargamos todas las imágenes de los cuadros de animación, de forma similar a como lo hicimos para la nave enemiga. Veamos el código:


...

# La clase CPlayer hereda de CSprite.
class CPlayer(CSprite):

   ...

   # ----------------------------------------------------------------
   # Constructor. Recibe el tipo de nave (TYPE_PLAYER_1 o TYPE_PLAYER_2).
   # ----------------------------------------------------------------
   def __init__(self, aType):
       CSprite.__init__(self)

       # Segun el tipo de la nave, la imagen que se carga.
       self.mType = aType

       if self.mType == CPlayer.TYPE_PLAYER_1:
           imgFile = "assets/images/player0"
       elif self.mType == CPlayer.TYPE_PLAYER_2:
           imgFile = "assets/images/player1"

       self.mFrames = []
       i = 0
       while (i <= 2):
           tmpImg = pygame.image.load(imgFile + str(i) + ".png")
           tmpImg = tmpImg.convert_alpha()
           self.mFrames.append(tmpImg)
           i = i + 1
          
       self.setImage(self.mFrames[0])

...

Como vemos en las imágenes, el índice cero del array tendrá la imagen correspondiente a la nave quieta, el índice uno corresponde a la nave moviéndose a la izquierda y el índice dos corresponde a la nave moviéndose a la derecha. Mira los nombres de las imágenes y el código para entender que esto es así.


Entonces, en la función update(), cuando la nave se mueve a la izquierda, se establece como imagen a self.mFrames[1], y cuando se mueve a la derecha se establece la imagen self.mFrames[2]. Cuando la nave se queda quieta, se establece la imagen self.mFrames[0]. El código de la función update() que implementa esta animación del jugador se muestra a continuación.


...

def update(self):

   # Obtener los controles.
   if self.mType == CPlayer.TYPE_PLAYER_1:
       left = CKeyboard.inst().leftPressed()
       right = CKeyboard.inst().rightPressed()
       fire = CKeyboard.inst().fire()
   else:
       left = CKeyboard.inst().APressed()
       right = CKeyboard.inst().DPressed()
       fire = CKeyboard.inst().fire2()

   # Mover la nave según las teclas.
   if (not left and not right):
       self.setVelX(0)
       self.setImage(self.mFrames[0])
   else:
       if left:
           self.setVelX(-4)
           self.setImage(self.mFrames[1])
       elif right:
           self.setVelX(4)
           self.setImage(self.mFrames[2])
...

Notemos que este tipo de animación es diferente al de la nave enemiga. En esta animación, en cada update() establecemos la imagen que corresponda a la dirección de movimiento del jugador, mientras que las naves enemigas tienen una animación continua.


También debemos notar que la clase CPlayer hereda de CSprite. Esto es así porque no tenemos una animación automática, sino que simplemente cambiamos la imagen a usar.


Nota: Un videojuego suele tener tanto sprites sin animaciones, como sprites con animaciones continuas y sprites con animaciones controladas de diversas formas. Una vez que hacemos algunas, el resto son parecidas. La primera vez será más trabajo implementarlas, pero luego es mucho más sencillo.



Optimizando la Memoria


Una cuestión importante a tener en cuenta es el manejo de memoria. Al principio cuando el juego es chico no hay problema, pero a medida que el juego crece, pueden aparecer problemas de falta de memoria si no tenemos cuidado con el uso de la memoria.


Por ejemplo, si observamos la clase CNave, vemos que al inicio se crea un array de imágenes, cargando las imágenes correspondientes a la animación de la nave. Esto está bien, pero, ¡esta carga de imágenes se hace para cada una de las naves del juego!


Esto hace que se desperdicie mucha memoria, dado que habrán varias naves que utilizarán los mismos gráficos, sin embargo, se están cargando las imágenes por cada nave. Lo mismo ocurre con las balas del jugador, con las balas enemigas, y en general con cualquier gráfico que se use en dos o más clases.


No es sencillo al principio solucionar esto, pero con el tiempo nos iremos dando cuenta de cuáles son las posibles optimizaciones y las iremos realizando.

Esto lo arreglaremos más adelante cuando implementemos un manager de assets. Esto es, una clase que se encargará de cargar todas las imágenes al inicio del juego (cuando se está mostrando una pantalla de carga). De esta forma, cada clase pedirá los gráficos necesarios para dibujar, pero estos gráficos se cargarán una única vez. Ningún gráfico del juego debe cargarse más de una vez. Lo mismo que hacemos con las imágenes se aplicará a todos los assets en general (imágenes, sonidos y fuentes).


Nota: Siempre es mejor implementar la funcionalidad de una parte determinada del juego antes de ponerse a optimizar. Con el tiempo se adquiere experiencia y ya se pueden prever ciertas situaciones, pero por ahora está bien programar sin pensar en la optimización y más adelante optimizar.


También recuerda que en la función destroy() debemos liberar de memoria todo lo que se ha creado en la función constructora. Por ejemplo, la clase CPlayer carga las imágenes en el array de frames, y la clase destroy() las elimina. Veamos esta función:


# Liberar lo que haya creado el objeto.
def destroy(self):

   # Invocar destroy() de CSprite.
   CSprite.destroy(self)

   i = len(self.mFrames)
   while i > 0:
       self.mFrames[i - 1] = None
       self.mFrames.pop(i - 1)
       i = i - 1
   mFrames = None

Como vemos, se recorre el array mFrames de imágenes, poniendo en None esa imagen, eliminando el elemento del array y por último poniendo la referencia del array en None. Con esto queda todo eliminado.


De esta forma terminamos el capítulo de animación de sprites. Ahora ya podemos implementar sprites animados. Pero para poder crear sprites con muchas animaciones y comportamientos, debemos aprender a programar los estados de los objetos del juego. Esto lo veremos en el siguiente capítulo.


179 visualizaciones0 comentarios

Entradas Recientes

Ver todo
bottom of page