top of page
Buscar
  • Foto del escritorFernando Sansberro

10. El Manager de Enemigos

Actualizado: 24 dic 2021

Así como en el capítulo anterior creamos balas y las controlamos con un manager, lo mismo haremos haremos con los enemigos. Crearemos entonces un manager para manejar a los enemigos. De esta forma, el código del juego quedará mucho más sencillo de leer, y cada clase hará solamente lo que le corresponde. El manager de enemigos se encargará del control de la lista de enemigos, al igual que lo hacía el manager de balas con las balas.


El Manager de Enemigos


Comenzaremos agregando en la carpeta api, una clase denominada CEnemyManager para que contenga la lista (o array) de enemigos. Lo que haremos es copiar la clase CBulletManager, dado que lo que hace es lo mismo por ahora. Luego mejoraremos esto, porque no es bueno tener dos clases cuyo código sea similar, pero de alguna forma hay que empezar.


Veamos el código del ejemplo que se encuentra en la carpeta capitulo_10\001_manager_de_enemigos.

El código de la clase CEnemyManager se encuentra en la carpeta api, y es el siguiente:


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

#-------------------------------------------------------------------
# Clase CEnemyManager.
# Clase para manejar los enemigos del juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#-----------------------------------------------------------------

# Importar Pygame.
import pygame

class CEnemyManager(object):

   mInstance = None
   mInitialized = False

   # Lista de enemigos.
   mEnemies = None

   def __new__(self, *args, **kargs):
       if (CEnemyManager.mInstance is None):
           CEnemyManager.mInstance = object.__new__(self, *args, **kargs)
           self.init(CEnemyManager.mInstance)
       else:
           print("Cuidado: CEnemyManager(): No se debería instanciar más de una vez esta clase. Usar CEnemyManager.inst().")
       return self.mInstance

   @classmethod
   def inst(cls):
       if (not cls.mInstance):
           return cls()
       return cls.mInstance
  
   def init(self):
       if (CEnemyManager.mInitialized):
           return
       CEnemyManager.mInitialized = True

       # Crear la lista de enemigos.
       CEnemyManager.mEnemies = []

   # Procesar los objetos.
   def update(self):
       for b in CEnemyManager.mEnemies:
           b.update()

       i = len(CEnemyManager.mEnemies)

       while i > 0:
           if CEnemyManager.mEnemies[i-1].isDead():
               CEnemyManager.mEnemies[i-1].destroy()
               CEnemyManager.mEnemies.pop(i-1)
               print("se elimina un enemigo")
           i = i - 1

       # Mostrar la cantidad de balas que hay en el manager.
       print(len(CEnemyManager.mEnemies))

   # Dibujar los objetos.
   def render(self, aScreen):
       for b in CEnemyManager.mEnemies:
           b.render(aScreen)

   # Agregar un objeto a la lista.
   def addEnemy(self, aEnemy):
       CEnemyManager.mEnemies.append(aEnemy)

   # Destruir todos los objetos y removerlos de la lista.
   # Destruir la lista.
   def destroy(self):
       i = len(CEnemyManager.mEnemies)
       while i > 0:
           CEnemyManager.mEnemies[i-1].destroy()
           CEnemyManager.mEnemies.pop(i-1)
           i = i - 1

       CEnemyManager.mInstance = None

Como vemos, el manager de enemigos es muy parecido al manager de balas que creamos en el capítulo anterior. De hecho, es el mismo código, salvo que cambia CBulletManager por CEnemyManager, el nombre del array mBullets cambia por mEnemies y la función addBullet() cambia por addEnemy(). Por ahora lo dejaremos así, y más adelante mejoraremos esto para no tener código duplicado.


Al igual que el manager de balas, el manager de enemigos es una clase que tiene un array con los enemigos que están actualmente en el juego. Usaremos la función addEnemy() para agregar un enemigo al manager, para que éste lo agregue a la lista de enemigos. Luego tendremos, como siempre, las funciones update(), render() y destroy() que se encargarán de recorrer el array de enemigos e invocar a la función update() y render() respectivamente para cada uno de los enemigos de la lista, e invocar a destroy() al eliminar el enemigo.


Entonces, la función update() del manager de enemigos corre la lógica de los enemigos, la función render() dibuja en pantalla todos los enemigos y la función destroy() que se llama al terminar el juego destruye a todos los enemigos que quedan en la lista. Todo esto es similar a lo que hicimos con las balas en el capítulo anterior.


Utilizando un manager, ya no tenemos necesidad de tener una variable para cada enemigo. El programa main.py, queda mucho más sencillo. Ya no tendremos las variables globales n1, n2, ...n5 que teníamos para cada nave enemiga, sino que las usaremos solo como variables temporales al crear los enemigos en la función init(). Luego de crear las naves, las agregamos al manager y se manejan automáticamente.

Cuando creamos los enemigos en la función init() en main.py, debemos agregarlos al manager de enemigos. Hay que tener en cuenta que si nos olvidamos de hacer esto, los enemigos no se van a procesar ni a dibujar.


...

# Importar el manager de enemigos.
from api.CEnemyManager import *

...

# Función de inicialización.
def init():

   ...

   # Crear las naves: se le pasa como parámetro la imagen de la nave.
   n1 = CNave(CNave.TYPE_PLATINUM)
   n2 = CNave(CNave.TYPE_GOLD)
   n3 = CNave(CNave.TYPE_RED)
   n4 = CNave(CNave.TYPE_GREEN)
   n5 = CNave(CNave.TYPE_CYAN)

   # Colocar las naves en su posición inicial.
   n1.setXY(0, 100)
   n2.setXY(0, 150)
   n3.setXY(0, 200)
   n4.setXY(0, 250)
   n5.setXY(0, 300)

   ...

   # Agregar las naves al manager.
   CEnemyManager.inst().addEnemy(n1)
   CEnemyManager.inst().addEnemy(n2)
   CEnemyManager.inst().addEnemy(n3)
   CEnemyManager.inst().addEnemy(n4)
   CEnemyManager.inst().addEnemy(n5)

   ...

La función update() en main.py, ya no tiene una llamada a update() para cada enemigo (las variables n1 a n5 han sido eliminadas), sino que simplemente hace una llamada a la función update() del manager de enemigos, y éste es quien se encarga de invocar update() a cada enemigo:


def update():

    ...

    # Actualizar los enemigos
    CEnemyManager.inst().update()

    ...

Del mismo modo, en la función render() de main.py, se llama a la función render() del manager de enemigos y ya no usaremos una variable para cada enemigo:


def render():

    ...

    # Dibujar los enemigos.
    CEnemyManager.inst().render(screen)

    ...

Lo mismo ocurre en la función destroy(), en lugar de llamar a la función destroy() para cada una de las variables que había para cada enemigo, se llama a la función destroy() del manager de enemigos:


def destroy():

    ...

    # Destruir los enemigos.
    CEnemyManager.inst().destroy()

    ...

Como vemos, al haber eliminado muchas variables, el código queda bastante más simple, y el hecho de agregar enemigos ahora es mucho más sencillo, facilitando el desarrollo.


Disparo de los Enemigos


Como estamos construyendo un juego de disparos (un shooter) como Space Invaders, hagamos ahora que los enemigos disparen.


Lo primero que necesitaremos es una clase para la bala enemiga. A esta clase la llamaremos CEnemyBullet, y el código será igual al código de la clase de la bala del jugador, excepto que usa un gráfico de una bala roja, para indicar que es peligrosa. Como es una clase del juego, esta clase irá en la carpeta game. La imagen de la bala, como siempre, la colocamos en la carpeta images en assets. La imagen de la bala se encuentra en el archivo enemy_bullet.png. El tamaño es igual que la bala del jugador (7x7 píxeles). Cópiala del ejemplo a tu propio proyecto.


Figura 10-1: La bala que va a disparar las nave enemiga.



Veamos el ejemplo ubicado en la carpeta capitulo_10\002_disparo_de_enemigos.

El código de la clase CEnemyBullet es muy simple, y lo único que hace es inicializar el sprite con la ruta de la imagen:


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

#-------------------------------------------------------------------
# Clase CEnemyBullet.
# Balas del enemigo.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------

# Importar Pygame.
import pygame

# La clase CEnemyBullet hereda de CSprite
from api.CSprite import *

class CEnemyBullet(CSprite):

   # Constructor.
   def __init__(self):
       CSprite.__init__(self, "assets/images/enemy_bullet.png")

   # Mover el objeto.
   def update(self):

       CSprite.update(self)

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

       CSprite.render(self, aScreen)

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

       CSprite.destroy(self) 

Nota: Ahora, mientras hacemos las clases por primera vez, vemos que hay código muy parecido, como es el caso entre las balas del jugador y las balas enemigas. Esto en un juego mediano no es así, dado que cada clase tendrá sus propios comportamientos, animaciones o características que las distingan unas de otra. Conviene tener una clase para cada objeto del juego siempre y cuando haya diferencias sustanciales con otros objetos. Y siempre hay que tratar de evitar tener código duplicado o código muy similar.


Teniendo la imagen de la bala en la carpeta de imágenes y la clase CEnemyBullet pronta, lo que queda es hacer que la nave enemiga dispare una bala cada cierto tiempo. Para hacer esto simple, haremos que con cierta aleatoriedad (random), la nave dispara una bala.


Para disparar una bala, lo que hacemos es crear una instancia de la clase CEnemyBullet, le establecemos su posición inicial, su velocidad y la agregamos al manager de enemigos (porque es una bala enemiga). El resto se hace solo, dado que el manager luego se encarga de mover la bala y de dibujarla.

En la función update() de la clase CNave, tenemos el siguiente código para disparar una bala cada cierto tiempo:


...

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

class CNave(Sprite):

    ...

    # Mover el objeto.

    def update(self):

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

        # Ver si la nave dispara.
        if random.randrange(1, 50) == 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().addEnemy(b)

    ...

Esta forma de disparar aleatoriamente no es la mejor forma de hacerlo y es temporal. En cada frame, si un número aleatorio entre 1 y 50 es igual a 1 (esa es una probabilidad de 1/50), se dispara una bala enemiga. En un futuro mejoraremos esto.



Figura 10-2: Las naves enemigas disparan y se vuelven peligrosas.



Al disparar, se crea una instancia de la clase CEnemyBullet, se coloca la bala en la posición inicial (veremos más sobre esto a continuación), se establece una velocidad vertical positiva para que la bala se mueva hacia abajo, se establecen los límites de la pantalla y se le establece la acción CGameObject.DIE para que cuando la bala toque un borde de la pantalla se muera. Por último, se agrega la bala al manager de enemigos.


Al igual que con las balas del jugador, cuando las balas de las naves tocan un borde, como fueron marcadas para morir, el manager las destruye y las elimina de la lista.


Para posicionar la bala, usamos la función setXY(), pero debemos hacer un cálculo, para saber la x y la y para colocar la bala debajo de la nave y en el medio. Mira la siguiente figura:



Figura 10-3: Colocando la bala en la posición inicial.



Como la coordenada (el punto de registro) de las imágenes es la esquina superior izquierda, debemos calcular que coordenadas (x, y) le ponemos a la bala para que aparezca en el medio de la nave y salga desde abajo de la misma. Como vemos, tenemos:


x = x de la nave + la mitad del ancho de la nave - la mitad del ancho de la bala.

y = y de la nave + el alto de la nave .


Esto, traducido al programa queda:


     b.setXY(self.getX() + self.getWidth()/2 - b.getWidth()/2, 
     self.getY() + self.getHeight())

Nota: En los videojuegos es muy común este tipo de operaciones para que las cosas aparezcan en el lugar correcto. Lleva tiempo programar muy prolijo como es este caso, pero programar prolijo y atendiendo a los detalles, hace que los juegos queden muy pulidos y se puedan adaptar rápidamente a los cambios para mejoras.


Inicialización de los Enemigos - La Formación


En esta sección vamos a implementar la formación inicial de los enemigos, es decir, al inicializar a los enemigos, los colocaremos en una posición determinada, creando una formación de naves enemigas. Veamos la siguiente figura.



Figura 10.4: La formación inicial de los enemigos.



Como ya tenemos implementado el manager de enemigos, lo único que debemos hacer es crear a cada enemigo y establecer su posición inicial, velocidad y tipo de nave. Esto lo hacemos en la función init(), en main.py, que es en donde ahora estamos creando a los enemigos.

Veamos el ejemplo que se encuentra ubicado en la carpeta capitulo_10\003_formacion_de_enemigos.


Como tenemos que colocar a cada enemigo en una posición en una grilla (porque la formación tiene forma de grilla), usaremos una estructura while dentro de otra, la primera corresponderá a la fila y la segunda a la columna (usamos la variables f para la fila y la variable c para la columna). Veamos el código que crea a los enemigos en formación:


def init():

    ...

    # Crear la formación inicial.
    f = 0
    while f <= 4:
        c = 0        
        while c <= 4:
            n = CNave(f)
            n.setXY(100 + (100 * c), 50 + (40 * f))
            n.setVelX(4)
            n.setVelY(0)
            n.setBounds(0, 0, SCREEN_WIDTH - n.getWidth(), 
                                 SCREEN_HEIGHT - n.getHeight())
            n.setBoundAction(CGameObject.BOUNCE)
            CEnemyManager.inst().addEnemy(n)
            c = c + 1
        f = f + 1

    ...

En la formación tendremos cinco filas y cinco columnas, y crearemos a los enemigos desde arriba hacia abajo y de izquierda a derecha. Entonces, el primer while corresponde a las filas (se usa la variable f por “fila”) y el segundo while corresponde a la columna (usando la variable c por “columna”). Para cada fila, recorremos las columnas.


Como la formación tiene cinco filas, la variable f irá desde cero a cuatro (son cinco iteraciones). Del mismo modo, para cada fila, la variable c irá desde cero a cuatro (siendo cinco iteraciones).


Al momento de crear la nave, se le pasa como parámetro el tipo de nave. Este valor coincide con el número de fila (ver los valores de las constantes en la clase CNave, donde CNave.TYPE_PLATINUM es 0, CNave.TYPE_GOLD es 1, etc).


Luego viene la sentencia n.setXY(100 + (70 * c), 50 + (35 * f)) para colocar a cada nave en su posición inicial. Aquí usamos varios números operados para calcular las coordenadas iniciales de la nave. Como vemos en el cálculo de la sentencia setXY(), tenemos varios valores a tener en cuenta: el margen horizontal desde el borde izquierdo de la pantalla (100 + ...) (denominado offset horizontal), el margen vertical desde la parte superior de la pantalla (50 + ...) (denominado offset vertical), el espacio horizontal entre las naves (70) y el espacio vertical entre las naves (35).


Todos estos valores se tienen en cuenta, junto con la columna y fila de la cual se trate, para calcular la posición inicial de las naves. Para calcular la coordenada x, se multiplica la columna por la separación entre naves y a eso se le suma el offset horizontal. Del mismo modo, para calcular la coordenada y, se multiplica la fila por la separación entre naves y a eso se le suma el offset vertical.



Figura 10.5: Ahora el juego tiene muchos enemigos ya formados.



Por último, a la nave le establecemos los límites de movimiento, le ponemos el comportamiento para rebotar como siempre, y agregamos el enemigo al manager. Es importante notar que el código de inicialización de los enemigos ha quedado ahora mucho más reducido, dado que las cosas se calculan y no se establecen los valores de a uno para cada nave, como se hacía antes.


Al correr el ejemplo vemos que la formación rebota y no baja toda junta como en el juego Space Invaders. Eso lo haremos más adelante.


Agregando otro Jugador - Modo Dos Players


Los videojuegos son mucho más divertidos cuando se juega con varios jugadores, por eso es que agregaremos un jugador más al juego. Implementaremos un modo de dos jugadores, en el cual uno controla una nave con color azul, utilizando las flechas para moverla y [Space] para disparar, y el segundo jugador manejará una nave de color rojo, utilizando las teclas [A] y [D] para moverse, y [Q] para disparar.

Figura 10-6: La nave azul es el primer jugador y la nave roja es el segundo jugador.



Veamos el ejemplo que se encuentra ubicado en la carpeta capitulo_10\004_dos_jugadores.

Lo primero que hacemos es colocar la imagen de la nave roja en la carpeta assets/images.

En el programa main.py, vamos a renombrar la variable player por player1, y agregamos una variable player2 para controlar al segundo jugador, y agregamos el código que lo crea e inicializa, luego se le corre update() y render() en el game loop, y se destruye al final con destroy(). En el siguiente código se muestra esto:


...

player1 = None
player2 = None

...

# Función de inicialización.
def init():

   ...

   global player1
   global player2

   ...

   # Crear el jugador 1.
   player1 = CPlayer(CPlayer.TYPE_PLAYER_1)
   player1.setXY(SCREEN_WIDTH / 4 - player1.getWidth() / 2, SCREEN_HEIGHT - player1.getHeight())
   player1.setBounds(0, 0, SCREEN_WIDTH - player1.getWidth(), SCREEN_HEIGHT)

   # Crear el jugador 2.
   player2 = CPlayer(CPlayer.TYPE_PLAYER_2)
   player2.setXY(SCREEN_WIDTH / 4 * 3 - player2.getWidth() / 2, SCREEN_HEIGHT - player2.getHeight())
   player2.setBounds(0, 0, SCREEN_WIDTH - player2.getWidth(), SCREEN_HEIGHT)

   ...

# Correr la lógica del juego.
def update():
   
   ...

   # Lógica de los jugadores.
   player1.update()
   player2.update()

   ...

# Dibujar el frame y actualizar la pantalla.
def render():
   
   ...

   # Dibujar los jugadores.
   player1.render(screen)
   player2.render(screen)

   ...

# Función de destrucción.
def destroy():
   global player1
   global player2

   # Destruir la nave de los jugadores.
   player1.destroy()
   player1 = None
   player2.destroy()
   player2 = None

   ...

Como vemos en la creación de los jugadores, le estamos pasando como parámetro el tipo de jugador del que se trata (CPlayer.TYPE_PLAYER_1 o CPlayer.TYPE_PLAYER_2). En la clase CPlayer, guardamos este valor en una variable mType, y según el valor que tenga manejaremos el jugador con un conjunto de teclas u otro. También según el tipo de jugador del que se trate, se cargará una imagen u otra.


A continuación se muestran los cambios realizados en la clase CPlayer.


...

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

   # Tipos de jugador.
   TYPE_PLAYER_1 = 0
   TYPE_PLAYER_2 = 1

   ...

   def __init__(self, aType):

       # 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"

       # Invocar al constructor de CSprite con la imagen a cargar.
       CSprite.__init__(self, imgFile)

   # Mover el objeto.
   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)
       else:
           if left:
               self.setVelX(-4)
           elif right:
               self.setVelX(4)

       # Disparar.
       if fire:
           print("FIRE")
           b = CPlayerBullet()
           b.setXY(self.getX() + self.getWidth() / 2 - b.getWidth() / 2, self.getY())
           b.setVelX(0)
           b.setVelY(-10)
           b.setBounds(0, 0, CGameConstants.SCREEN_WIDTH, CGameConstants.SCREEN_HEIGHT)
           b.setBoundAction(CGameObject.DIE)
           CBulletManager.inst().addBullet(b)

       ...

Como vemos en el código de update() de CPlayer, según el tipo de nave del que se trate, usamos controles diferentes para mover el jugador. Como ahora necesitamos leer más teclas, tenemos que agregar el control de estas teclas en la clase CKeyboard.


En la clase CKeyboard agregamos variables para leer las teclas de movimiento que necesitamos. Más adelante debemos terminar de escribir la clase CKeyboard para que sirva para poder saber si cualquier tecla del teclado está siendo pulsada, si está suelta, etc.

Aprovechamos y agregamos funciones para detectar las teclas de direcciones usando [Izquierda], [Derecha], [Arriba], [Abajo] y [W], [A], [S], [D]. Para disparar, se usan las funciones fire1() y fire2(), para el jugador uno y dos respectivamente, que chequean cuando las teclas de disparo se pulsan por primera vez. Para disparar usamos [Space] para el jugador uno y [Q] para el jugador dos.

Examina en el programa, el código de la clase CKeyboard para ver estas funciones, que son similares a las que ya habían sido implementadas.


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

#--------------------------------------------------------------------
# Clase CKeyboard.
# Clase para manejar el estado de las teclas en el juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------

# Importar Pygame.
import pygame

class CKeyboard(object):

   mInstance = None
   mInitialized = False

   mLeftPressed = False
   mRightPressed = False
   mUpPressed = False
   mDownPressed = False
   mAPressed = False
   mDPressed = False
   mWPressed = False
   mSPressed = False

   # Estado de la tecla [Space] en el frame anterior.
   mSpacePressedPreviousFrame = False
   mSpacePressed = False
   # Estado de la tecla [Q] en el frame anterior.
   mQPressedPreviousFrame = False
   mQPressed = False

   def __new__(self, *args, **kargs):
       if (CKeyboard.mInstance is None):
           CKeyboard.mInstance = object.__new__(self, *args, **kargs)
           self.init(CKeyboard.mInstance)
       else:
           print("Cuidado: CKeyboard(): No se debería instanciar más de una vez esta clase. Usar CKeyboard.inst().")
       return CKeyboard.mInstance

   @classmethod
   def inst(cls):
       if (not cls.mInstance):
           return cls()
       return cls.mInstance
  
   def init(self):
       if (CKeyboard.mInitialized):
           return
       CKeyboard.mInitialized = True

       CKeyboard.mLeftPressed = False
       CKeyboard.mRightPressed = False
       CKeyboard.mUpPressed = False
       CKeyboard.mDownPressed = False
       CKeyboard.mAPressed = False
       CKeyboard.mDPressed = False
       CKeyboard.mWPressed = False
       CKeyboard.mSPressed = False

       CKeyboard.mSpacePressedPreviousFrame = False
       CKeyboard.mSpacePressed = False
       CKeyboard.mQPressedPreviousFrame = False
       CKeyboard.mQPressed = False
      
   def keyDown(self, key):
       if (key == pygame.K_LEFT):
           CKeyboard.mLeftPressed = True
       if (key == pygame.K_RIGHT):
           CKeyboard.mRightPressed = True
       if (key == pygame.K_UP):
           CKeyboard.mUpPressed = True
       if (key == pygame.K_DOWN):
           CKeyboard.mDownPressed = True
       if (key == pygame.K_SPACE):
           CKeyboard.mSpacePressed = True
       if (key == pygame.K_a):
           CKeyboard.mAPressed = True
       if (key == pygame.K_d):
           CKeyboard.mDPressed = True
       if (key == pygame.K_w):
           CKeyboard.mWPressed = True
       if (key == pygame.K_s):
           CKeyboard.mSPressed = True
       if (key == pygame.K_q):
           CKeyboard.mQPressed = True

   def keyUp(self, key):
       if (key == pygame.K_LEFT):
           CKeyboard.mLeftPressed = False
       if (key == pygame.K_RIGHT):
           CKeyboard.mRightPressed = False
       if (key == pygame.K_UP):
           CKeyboard.mUpPressed = False
       if (key == pygame.K_DOWN):
           CKeyboard.mDownPressed = False
       if (key == pygame.K_SPACE):
           CKeyboard.mSpacePressed = False
       if (key == pygame.K_a):
           CKeyboard.mAPressed = False
       if (key == pygame.K_d):
           CKeyboard.mDPressed = False
       if (key == pygame.K_w):
           CKeyboard.mWPressed = False
       if (key == pygame.K_s):
           CKeyboard.mSPressed = False
       if (key == pygame.K_q):
           CKeyboard.mQPressed = False

   # Actualiza el estado de la tecla [Space].
   def update(self):
       CKeyboard.mSpacePressedPreviousFrame = CKeyboard.mSpacePressed
       CKeyboard.mQPressedPreviousFrame = CKeyboard.mQPressed

   def leftPressed(self):
       return CKeyboard.mLeftPressed

   def rightPressed(self):
       return CKeyboard.mRightPressed

   def upPressed(self):
       return CKeyboard.mUpPressed

   def downPressed(self):
       return CKeyboard.mDownPressed

   def spacePressed(self):
       return CKeyboard.mSpacePressed

   def APressed(self):
       return CKeyboard.mAPressed

   def DPressed(self):
       return CKeyboard.mDPressed

   def WPressed(self):
       return CKeyboard.mWPressed

   def SPressed(self):
       return CKeyboard.mSPressed

   def QPressed(self):
       return CKeyboard.mQPressed

   # Función usada para disparar.
   # Solo retorna True en el momento en que se apreta la tecla.
   def fire(self):
       return CKeyboard.mSpacePressed == True and CKeyboard.mSpacePressedPreviousFrame == False

   # Función usada para disparar.
   # Solo retorna True en el momento en que se apreta la tecla.
   def fire2(self):
       return CKeyboard.mQPressed == True and CKeyboard.mQPressedPreviousFrame == False

   def destroy(self):
       CKeyboard.mInstance = None

Por último, como un detalle, en la clase CNave, hemos cambiado la frecuencia (la probabilidad) con la que disparan las naves enemigas, porque era muy difícil esquivar las balas. Cambiamos 50 por 500 para que tenga menos chance de disparar.


Unificando los Managers


En este momento en el proyecto tenemos dos managers que son prácticamente iguales (el manager de enemigos y el manager de balas), y esto no es una buena práctica de diseño, dado que lleva a tener dos clases con el código repetido o muy similar. Debemos siempre evitar este tipo de cosas, porque hacen al código menos mantenible. Por ejemplo, si quisiéramos modificar el comportamiento de una clase, seguramente tengamos que modificar en más de un lugar a la vez.


Nota: Un videojuego es un sistema que requiere de cambios frecuentes en la funcionalidad. Muchas cosas que programamos no sabremos si funcionan bien hasta no probarlas en el juego. Otras cosas se planifican de una manera pero en el juego funcionan mejor de otra forma, y esto se va mejorando en sucesivas versiones del juego, a las que se le denomina iteraciones del desarrollo.


Lo que haremos es poner el código común en una clase base, denominada CManager, y luego tener dos clases, una para cada manager: CEnemyManager y CBulletManager. Podríamos tener una sola clase, pero como es probable que más adelante tengamos código específico para el manager de los enemigos y código específico para el manager de las balas, mejor haremos dos clases aunque por ahora queden casi vacías.


Examina el ejemplo de la carpeta capitulo_10\005_managers_enemigos_disparos_unificados.

Este ejemplo es igual al anterior, pero se ha agregado una clase CManager que tiene un array de elementos (balas o enemigos según el caso) y las funciones update(), render() y destroy() como es habitual. Luego, las clases CEnemyManager y CBulletManager son clases singleton que heredan de la clase CManager.


El código de la case CManager se lista a continuación:


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

#--------------------------------------------------------------------
# Clase Manager.
# Clase para manejar una lista con un tipo de objeto.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------

# Importar Pygame.
import pygame

class CManager(object):

   def __init__(self):

       # Lista de objetos.
       self.mArray = []

   # Procesar los objetos.
   def update(self):
       for e in self.mArray:
           e.update()

       i = len(self.mArray)
       while i > 0:
           if self.mArray[i-1].isDead():
               self.mArray[i-1].destroy()
               self.mArray.pop(i-1)
           i = i - 1

   # Dibujar los objetos.
   def render(self, aScreen):
       for e in self.mArray:
           e.render(aScreen)

   # Agregar un objeto a la lista.
   def add(self, aElement):
       self.mArray.append(aElement)

   # Destruir todos los objetos y removerlos de la lista.
   # Destruir la lista.
   def destroy(self):
       i = len(self.mArray)
       while i > 0:
           self.mArray[i-1].destroy()
           self.mArray.pop(i-1)
           i = i - 1

       self.mArray = None

De esta forma, el código que maneja el array ya no se encuentra duplicado en ambas clases como antes, sino que se encuentra centralizado ahora en la clase CManager. Antes una clase tenía un array llamado mBullets y la otra clase tenía un array llamado mEnemies. Ahora existe un solo código centralizado y el array se llama mArray. Veamos el código de ambos manager y veremos que queda mucho más simple. El código de CEnemyManager queda de la siguiente manera:


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

#--------------------------------------------------------------------
# Clase CEnemyManager.
# Clase para manejar los enemigos del juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#-------------------------------------------------------------------

# Importar Pygame.
import pygame

# Importar la clase base del manager.
from api.CManager import *

class CEnemyManager(CManager):

   mInstance = None
   mInitialized = False

   def __new__(self, *args, **kargs):
       if (CEnemyManager.mInstance is None):
           CEnemyManager.mInstance = object.__new__(self, *args, **kargs)
           self.init(CEnemyManager.mInstance)
       else:
           print("Cuidado: CEnemyManager(): No se debería instanciar más de una vez esta clase. Usar CEnemyManager.inst().")
       return self.mInstance

   @classmethod
   def inst(cls):
       if (not cls.mInstance):
           return cls()
       return cls.mInstance
  
   def init(self):
       if (CEnemyManager.mInitialized):
           return
       CEnemyManager.mInitialized = True

       # Invocar al constructor de la clase base que crea la lista.
       CManager.__init__(self)

   # Procesar los objetos.
   def update(self):
       CManager.update(self)

   # Dibujar los objetos.
   def render(self, aScreen):
       CManager.render(self, aScreen)

   # Agregar un objeto a la lista.
   def add(self, aEnemy):
       CManager.add(self, aEnemy)

   # Destruir todos los objetos y removerlos de la lista.
   # Destruir la lista.
   def destroy(self):
       CManager.destroy(self)

       CEnemyManager.mInstance = None

El código de CBulletManager queda de la siguiente manera:


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

#--------------------------------------------------------------------
# Clase CBulletManager.
# Clase para manejar los disparos del juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------

# Importar Pygame.
import pygame

# Importar la clase base del manager.
from api.CManager import *

class CBulletManager(CManager):

   mInstance = None
   mInitialized = False

   def __new__(self, *args, **kargs):
       if (CBulletManager.mInstance is None):
           CBulletManager.mInstance = object.__new__(self, *args, **kargs)
           self.init(CBulletManager.mInstance)
       else:
           print("Cuidado: CBulletManager(): No se debería instanciar más de una vez esta clase. Usar CBulletManager.inst().")
       return self.mInstance

   @classmethod
   def inst(cls):
       if (not cls.mInstance):
           return cls()
       return cls.mInstance
  
   def init(self):
       if (CBulletManager.mInitialized):
           return
       CBulletManager.mInitialized = True

       # Invocar al constructor de la clase base que crea la lista.
       CManager.__init__(self)

   # Procesar los objetos.
   def update(self):
       CManager.update(self)

   # Dibujar los objetos.
   def render(self, aScreen):
       CManager.render(self, aScreen)

   # Agregar un objeto a la lista.
   def add(self, aBullet):
       CManager.add(self, aBullet)

   # Destruir todos los objetos y removerlos de la lista.
   # Destruir la lista.
   def destroy(self):
       CManager.destroy(self)

       CBulletManager.mInstance = None

Para agregar naves al manager de enemigos (en main.py), y para agregar balas al manager de balas, tanto al disparar en el jugador como en las naves enemigas (clases CPlayer y CNave respectivamente), usamos la función add() del manager, en lugar de tener que usar addEnemy() o addBullet(). De esta forma el código también queda más sencillo. Debes cambiar en estas clases estas llamadas por add().


Recuerda que no es bueno tener código duplicado, porque esto hace menos mantenible el proyecto. Esta es la razón principal detrás de este cambio que hemos realizado.


Con esto finalizamos el manejo del manager de enemigos y del manager general. Ahora como vemos al ejecutar el juego, hay muchas balas pero ninguna hace efecto. En el siguiente capítulo veremos cómo hacer las colisiones entre objetos.




10 visualizaciones0 comentarios

Entradas recientes

Ver todo
bottom of page