top of page
Buscar

9. Disparos y el Manager de Sprites

  • Foto del escritor: Fernando Sansberro
    Fernando Sansberro
  • 6 nov 2021
  • 15 Min. de lectura

Actualizado: 24 dic 2021

Un videojuego tipo arcade que se precie, como el que estamos haciendo (una versión del clásico Space Invaders), necesita que el jugador dispare muchas balas para matar a los enemigos. En este capítulo veremos cómo armar un array de balas y una clase para manejarlas, a lo que le llamaremos el manager de balas. Esto es, una clase que se encarga del manejo automático de todas las balas del juego.


Disparando Balas


Para lograr que la nave del jugador dispare balas, tenemos que realizar varias tareas, dado que es la primera vez que hacemos que un objeto dispare.


Primero, debemos crear una clase para la bala, a la que le llamaremos CPlayerBullet. Luego, como el juego tendrá muchas balas, deberemos pensar en alguna forma de manejar las balas, esto es, agregarlas a una lista, correrle la lógica y dibujar todas las balas que hay en el juego, eliminar una bala de la lista cuando se va de la pantalla, etc. Para esto, haremos una clase que llamaremos CBulletManager, la cual se encargará de estas tareas.

Una vez que tenemos la clase para la bala y el manager de balas, lo que haremos será que el jugador dispare con la tecla [Space], creando en ese momento una bala y agregándola al manager de balas.


Corre el ejemplo que se encuentra en la carpeta capitulo_09\001_manager_de_balas. En este ejemplo, la nave dispara muchas balas cuando se pulsa la tecla [Space]. Todavía las balas no se eliminan cuando se van de la pantalla y como puedes observar, si dejamos la tecla de disparo pulsada, aparecen balas continuamente. Estas cuestiones las iremos corrigiendo a lo largo de este capítulo.

Primero veamos la clase para la bala, que se encuentra en CPlayerBullet.py. Esta clase no es nada particular. Simplemente es un sprite (hereda de CSprite) que carga la imagen de la bala. Esta clase se encuentra en la carpeta game, debido a que es un objeto del juego.


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

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

# Importar Pygame.
import pygame

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

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

   # Constructor.
   def __init__(self):
       CSprite.__init__(self, "assets/images/player_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) 

La imagen que se usa para la bala es player_bullet.png y como siempre, la colocamos en la carpeta assets/images. Cópiala de los ejemplos a tu propio proyecto. El tamaño de la bala es de 7x7 píxeles.



Figura 9-1: La bala que va a disparar nuestra nave.



En la clase CPlayer, en la función update(), agregamos el código que chequea si la tecla de disparo ([Space]) está pulsada. Si esto es así, se crea la bala.


# Mover el objeto.
def update(self):

   ...

   # Disparar con la tecla [Space].
   if CKeyboard.inst().spacePressed():
       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, 640, 360)
       b.setBoundAction(CGameObject.DIE)
       CBulletManager.inst().addBullet(b)

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

Como siempre, cuando vamos a usar una clase (en este caso, CPlayerBullet y CBulletManager), debemos importarlas al inicio de la clase:


# Importar el manager de balas.
from api.CBulletManager import *

# Importar la clase para la bala.
from game.CPlayerBullet import *

Como vemos en el código que dispara, primero se crea una bala, luego se establece su posición inicial (observar que se coloca la bala en la punta de la nave), y se establece su velocidad. Al darle una velocidad negativa en y, la bala saldrá hacia arriba.

Al pulsar la tecla [Space], la nave dispara. Para poder detectar esta tecla, se ha agregado la función spacePressed() en la clase CKeyboard. Mira el código de CKeyboard para ver el cambio realizado.


...

class CKeyboard(object):

   mInstance = None
   mInitialized = False

   mLeftPressed = False
   mRightPressed = False
   mSpacePressed = False

   ...
  
   def init(self):
       if (CKeyboard.mInitialized):
           return
       CKeyboard.mInitialized = True
      
       CKeyboard.mLeftPressed = False
       CKeyboard.mRightPressed = False
       CKeyboard.mSpacePressed = 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_SPACE):
           CKeyboard.mSpacePressed = 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_SPACE):
           CKeyboard.mSpacePressed = False

   def leftPressed(self):
       return CKeyboard.mLeftPressed

   def rightPressed(self):
       return CKeyboard.mRightPressed

   def spacePressed(self):
       return CKeyboard.mSpacePressed

   def destroy(self):
       CKeyboard.mInstance = None

Al disparar, con la función setBounds() le indicamos a la bala creada los límites de la pantalla, y le colocamos el comportamiento de borde CGameObject.DIE para que cuando la bala toque el borde de la pantalla sea eliminada. Este será el comportamiento típico para las balas


Nota: Es muy importante eliminar las balas que se van de la pantalla o de los bordes del mundo, porque en un juego de acción en el que disparamos mucho, si no se eliminan las balas, el juego se tornaría lento y usaría mucha memoria.


La última línea del disparo, CBulletManager.inst().addBullet(b), agrega la bala al manager de bala. El manager de balas es la clase que se encargará de agregar las balas a una lista, se encargará de procesar las balas (invocar a su update()), de dibujarlas (invocar a su render()) y de eliminarla de la lista cuando la bala sea destruida. A continuación veremos esta clase.


El Manager de Balas


Como podemos ver en el programa main.py, tenemos una variable para controlar cada una de las naves enemigas. Imagina si el juego tuviera cincuenta naves enemigas. Eso quiere decir que deberíamos repetir el código con cincuenta variables diferentes, lo cual estaría muy mal.


Como ahora vamos a disparar muchas balas, no podemos tener una variable para cada una de las balas, sino que haremos un array o lista, en el cual cada elemento del array será una bala (un objeto de clase CPlayerBullet). Haremos una clase que contenga este array, y esta clase tendrá funciones para agregar una bala a la lista, eliminarla de la lista, procesar las balas, etc.


Cuando tenemos una clase que se encarga, como en este caso, del manejo de todas las balas, le llamamos manager. El manager de balas es una clase que contiene un array con las balas existentes en el juego, y el manager se encarga de actualizar y dibujar cada una de las balas. Más adelante haremos un manager para cada tipo de objetos (enemigos, partículas, etc.).

En la siguiente figura vemos una representación gráfica del manager de balas. Cada elemento del array es una referencia a un sprite, en este caso, un objeto CPlayerBullet.



Figura 9-2: El manager es un array, donde cada elemento es una referencia a un objeto.



Del mismo modo, implementaremos más adelante un manager para manejar a los enemigos, momento en el que sacaremos todas las variables que ahora tenemos en main.py.



Figura 9-3: Más adelante tendremos un manager (un array) para los enemigos.



Veamos entonces el código de la clase CBulletManager, que hemos colocado en \api porque es una clase que reutilizaremos en todos nuestros juegos.


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

class CBulletManager(object):

   mInstance = None
   mInitialized = False

   # Lista de balas.
   mBullets = None

   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

       # Crear la lista de balas.
       CBulletManager.mBullets = []

   # Procesar los objetos.
   def update(self):
       for b in CBulletManager.mBullets:
           b.update()

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

   # Agregar un objeto a la lista.
   def addBullet(self, aBullet):
       CBulletManager.mBullets.append(aBullet)

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

         CBulletManager.mInstance = None

Como CBulletManager es una clase que se encarga de todas las balas en el juego, habrá una sola clase de este tipo. Por lo tanto, ha sido diseñada como singleton. Como siempre, la primera parte de un singleton se encarga de que no pueda crearse otra clase de ese tipo.


El manager contiene un array, llamado mBullets, que contendrá las balas en el juego (objetos de clase CPlayerBullet). La función addBullet() recibe como parámetro la bala creada al momento en el que el jugador dispara, y la agrega al array usando la función append(). De esta forma agregamos una bala a la lista, al final de la misma.


# Agregar un objeto a la lista.
def addBullet(self, aBullet):
CBulletManager.mBullets.append(aBullet)

Ahora, tendremos todas las balas que se encuentran vivas, en el array mBullets. Para actualizar la lógica de cada una, debemos llamar a la función update() de cada una de las balas. Lo mismo haremos para dibujar cada bala, llamando a la función render() de todas las balas.


Entonces, se implementa en el manager una función update() y otra render() que lo que hacen es recorrer la lista de balas e invocan a la función update() para correr la lógica de cada bala y render() para dibujar cada bala.


# Procesar los objetos.
def update(self):
   for b in CBulletManager.mBullets:
       b.update()

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

Este concepto es clave. La función update() de un manager se encarga de invocar a la función update() de cada uno de los elementos. Del mismo modo, la función render() del manager dibuja todos los elementos del array, invocando a la función render() para cada uno.


Por último, en el programa main.py, debemos invocar a las funciones update() y render() del manager de balas para actualizar y dibujar todas las balas.

Al final de todo, al terminar el programa, debemos siempre llamar a la función destroy() para que elimine todos los objetos que quedaron creados. Veamos la parte de main.py que invoca a funciones del manager:


...

# Importar el manager de balas.
from api.CBulletManager import *

...

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

   ...

   # Mover las balas.
   CBulletManager.inst().update()

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

   # Dibujar las balas.
   CBulletManager.inst().render(screen)

   ...

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

   # Destruir las balas.
   CBulletManager.inst().destroy()

   # Cerrar Pygame y liberar los recursos que pidió el programa.
   pygame.quit()

...

Cuando corremos el ejemplo, vemos que aún quedan tres cosas por corregir. Primero, cuando disparamos, las balas al llegar al borde de la pantalla no se eliminan. Más adelante en este capítulo arreglaremos este problema.



Figura 9-4: La nave del jugador ahora dispara balas.



El segundo problema que existe es un problema de diseño. Cuando en la clase CPlayer se dispara una bala, se le establecen los límites de la pantalla, pero desde la clase CPlayer no tenemos acceso al ancho y alto de la pantalla (esto está definido en el programa main.py). Este problema es bastante común al desarrollar videojuegos, que es que algunos datos los ponemos en una clase y luego no los tenemos disponibles para otras que lo necesitan. Esto lo solucionaremos más adelante en este capítulo y la solución es tener una clase donde guardaremos valores constantes para que todas las clases las puedan usar.


El tercer problema es que hemos utilizado la función spacePressed() de CKeyboard, que dice si la tecla [Space] está siendo pulsada o no. Pero esto no nos sirve, porque cuando pulsamos [Space] para disparar, la tecla está siendo pulsada en varios frames consecutivos (recordemos que el juego corre a 60 frames por segundo). Es imposible apretar y soltar la tecla en un solo frame, y es por eso que durante varios frames la tecla está pulsada, motivo por el cual salen varias balas seguidas. Arreglaremos esto a continuación.


La función fire() de CKeyboard


Como vimos en el ejemplo anterior, salen muchas balas cuando dejamos pulsada la tecla de disparo. Queremos hacer que cuando el jugador pulse [Space] para disparar, salga una sola bala y no vuelva a salir otra bala hasta que el jugador no suelte la tecla y la vuelva a pulsar.


Para hacer esto, ya no usaremos la función spacePressed() de CKeyboard, sino que haremos una función que llamaremos fire(), que retornará True en el momento en que se toca la tecla [Space] por primera vez y no vuelve a retornar True hasta que se suelte la tecla y se vuelva a presionar.


Abre y ejecuta el ejemplo que se encuentra en la carpeta capitulo_09\002_manager_de_balas_fire_keyboard.

Aquí vemos que la bala sale solamente cuando se pulsa tecla [Space], pero no vuelve a disparar hasta que no se levante la tecla y se vuelva a presionar. En la clase CPlayer, en la función update(), cambiamos spacePressed() por fire():


# Disparar con la tecla [Space].
if CKeyboard.inst().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, 640, 360)
   b.setBoundAction(CGameObject.DIE)
   CBulletManager.inst().addBullet(b)

En la clase CKeyboard, implementamos la función fire(), que debe retornar True solamente en el momento en que apretamos la tecla [Space] y si en el frame anterior no estaba pulsada. En cualquier otro caso esta función retornará False.

El siguiente es el código de CKeyboard y se han resaltado los cambios para implementar esto.


...

class CKeyboard(object):

   mInstance = None
   mInitialized = False

   mLeftPressed = False
   mRightPressed = False
   # Estado de la tecla [Space] en el frame anterior.
   mSpacePressedPreviousFrame = False
   mSpacePressed = False

   ...
  
   def init(self):
       if (CKeyboard.mInitialized):
           return
       CKeyboard.mInitialized = True
      
       CKeyboard.mLeftPressed = False
       CKeyboard.mRightPressed = False
       CKeyboard.mSpacePressedPreviousFrame = False
       CKeyboard.mSpacePressed = 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_SPACE):
           CKeyboard.mSpacePressed = 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_SPACE):
           CKeyboard.mSpacePressed = False

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

   def leftPressed(self):
       return CKeyboard.mLeftPressed

   def rightPressed(self):
       return CKeyboard.mRightPressed

   def spacePressed(self):
       return CKeyboard.mSpacePressed

   # 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

   def destroy(self):
       CKeyboard.mInstance = None

Agregamos una variable mSpacePressedPreviousFrame, que guarda el estado de la tecla [Space] para ser usado en el siguiente frame. Esto equivale a decir que esta variable almacena el estado de la tecla [Space] del frame anterior.


La clase CKeyboard ahora necesita una función update() dado que en cada frame guarda en esta variable el estado de la tecla [Space].

Como vemos en la función fire(), la misma retorna True si la tecla [Space] está siendo pulsada y en el frame anterior no había sido pulsada. Solo en ese momento es que fire() retorna True. En ese momento podemos disparar la bala.

Para que esto funcione, debemos llamar a la función update() de CKeyboard desde main.py:


def update():
    global salir
    global screen
    global isFullscreen

    clock.tick(60)

    # Llamar a update() de CKeyboard.
    CKeyboard.inst().update()

    ...

Al ejecutar el programa, vemos que la nave ahora dispara bien. Ya no salen tantas balas juntas. Solamente sale una bala cada vez que se presiona la tecla de disparo.


A medida que vayamos desarrollando nuestro primer videojuego, y luego otros, iremos viendo que necesitaremos control de más teclas, control del mouse, etc. Necesitaremos ir mejorando la clase CKeyboard agregándole funcionalidad. Implementaremos esta clase en forma completa más adelante, así como también otras clases que formarán nuestra base para desarrollar videojuegos (denominada API o framework).


Eliminando Balas del Manager


Cuando una bala se va de la pantalla, ésta debe ser eliminada. Para que esto funcione, necesitamos dos partes. La primera es al momento de crear la bala, definir el comportamiento de borde CGameObject.DIE. Esto ya lo estamos haciendo, mira el código de CPlayer al disparar:


# Disparar con la tecla [Space].
     if CKeyboard.inst().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, 640, 360)
         b.setBoundAction(CGameObject.DIE)
         CBulletManager.inst().addBullet(b)

La segunda parte es cuando la bala toca el borde, como tiene el comportamiento CGameObject.DIE, tenemos que programar para que la bala sea eliminada. Hasta ahora habíamos puesto un mensaje “objeto debe morir”, y es hora de que realmente sea eliminado. Esto lo haremos en la clase CGameObject.


Si corremos el ejemplo que se encuentra en la carpeta capitulo_09\003_manager_de_balas_eliminar_balas, veremos que las balas que tocan el borde de la pantalla son eliminadas.


En la clase CGameObject, agregamos una variable mIsDead que comenzará en False (el objeto comienza vivo) y valdrá True en el momento que toque un borde (si tiene establecido el comportamiento de borde CGameObject.DIE).


...

class CGameObject(object):

   ...

   def __init__(self):

       ...

       # Indica si el objeto está vivo o muerto (debe morir).
       self.mIsDead = False

   ...

   def checkBounds(self):

       ...

       # Si el comportamiento es que muera, se marca para morir.
       # El manager luego lo elimina.
       if (self.mBoundAction == CGameObject.DIE):
           self.mIsDead = True
           return

   # Indica si hay que eliminar el objeto o no.
   def isDead(self):
       return self.mIsDead

   ...  

Como vemos en la función checkBounds(), cuando el objeto toca el borde y tiene el comportamiento CGameObject.DIE, se marca el objeto para morir, asignando True a la variable mIsDead. Luego utilizamos la función isDead() para saber si el objeto está marcado para morir o no.


Por sí solo, esto no hace nada. Es el manager (CBulletManager.py) quien se encarga de eliminar el objeto, cuando luego de correrle la lógica, el objeto quedó marcado para morir. En la función update() del manager, se invoca a la función update() de cada una de las balas mediante una sentencia for. El agregado, es que luego de hacer esto, se vuelve a recorrer todas las balas del manager, preguntando si la bala está marcada para morir, y si esto es así, se elimina:


# Procesar los objetos.
def update(self):
   for b in CBulletManager.mBullets:
       b.update()

   i = len(CBulletManager.mBullets)

   # Eliminar las balas marcadas para morir.
   while i > 0:
       if CBulletManager.mBullets[i - 1].isDead():
           CBulletManager.mBullets[i - 1].destroy()
           CBulletManager.mBullets.pop(i - 1)
           print(“se elimina una bala”)
       i = i - 1

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

De esta forma cuando a una bala le colocamos el comportamiento de borde CGameObject.DIE, al llegar al borde de la pantalla se marca para morir, colocando la variable mIsDead en True. Esto automáticamente hace que el objeto sea eliminado, por el código que hemos agregado en la función update() del manager (el código que acabamos de ver).


Dicho de otra manera, hemos implementado un sistema sencillo que permite eliminar los objetos del juego automáticamente, simplemente prendiéndole una variable (poniendola en True). El manager se ocupa del resto.


Para eliminar un elemento del array, utilizamos la función pop() de arrays, que recibe como parámetro el índice del elemento a borrar.

Como nota, si a pop() no le pasamos parámetro, elimina el último elemento del array. Debes consultar la documentación de Python cuando quieras hacer algo y no sepas con qué función se hace.


Nota: El array se recorre desde el final hacia el principio, porque utilizamos una función pop() que elimina el elemento dado y corre el array hacia atrás. De esta forma los elementos anteriores no se tocan y se puede seguir la recorrida del array en forma segura.


Destruyendo los Objetos


Como norma general, cada vez que creamos un objeto, al final debemos liberar la memoria que utiliza, lo que se conoce básicamente como destruir el objeto.

Por ejemplo, debemos agregar código a la función destroy() de CBulletManager para que elimine todos los elementos que quedan en el array:

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

   CBulletManager.mInstance = None

Es necesario eliminar todas las balas que quedan en el array, porque no sabemos en qué momento vamos a perder o ganar, y es muy probable que al momento de pasar de pantalla y eliminar los objetos que hay en el juego, todavía queden balas en el mundo.


Al eliminar un objeto, siempre le invocamos la función destroy() para que el objeto elimine lo que haya creado, y luego se usa la función pop() sobre el array para eliminar el elemento del array del manager.


La Clase CGameConstants


Ahora ya tenemos el manager completo, y cada bala que dispare el jugador, se eliminará al tocar un borde de la pantalla. Lo que nos queda hacer por ahora es mejorar un problema de diseño que tiene el juego.


Cuando el jugador dispara, en la clase CPlayer, hace lo siguiente:


# Disparar con la tecla [Space].
if CKeyboard.inst().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, 640, 360)
   	b.setBoundAction(CGameObject.DIE)
   	CBulletManager.inst().addBullet(b)

Como vemos, le estamos estableciendo los límites de pantalla con dos números (640 y 460), pero no es una buena práctica de programación poner estos valores aquí, ya que de cambiar el tamaño de la pantalla, debemos cambiar todos estos valores en el código, en todas las partes donde los hayamos usado. En este momento lo tenemos aquí, y también lo tenemos en main.py, al crear la ventana. Si no tenemos cuidado, acabaremos con estos números repetidos en varias partes, y eso no es bueno.


Para corregir esto, haremos una clase en la cual pondremos todas las constantes del juego (los valores que no cambian). A esta clase la llamaremos CGameConstants, y como queremos poder acceder a estas constantes desde cualquier clase en el juego, esta clase será un singleton.

Veamos el código del ejemplo ubicado en la carpeta capitulo_09\004_game_constants.

El código de la clase CGameConstants es simplemente una clase que tiene constantes:


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

#-------------------------------------------------------------------
# Clase CGameConstants.
# Constantes globales para el juego. Tiene valores estáticos.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#-------------------------------------------------------------------

import pygame

class CGameConstants(object):

   SCREEN_WIDTH = 640
   SCREEN_HEIGHT = 360

En main.py, en lugar de utilizar los valores 640 y 360 directamente, lo que hacemos es leerlos desde la clase de constantes:


# Importar la clase CGameConstants
from api.CGameConstants import *

# Definir ancho y alto de la pantalla.
SCREEN_WIDTH = CGameConstants.SCREEN_WIDTH
SCREEN_HEIGHT = CGameConstants.SCREEN_HEIGHT

En la clase CPlayer, en el momento de disparar, también se leen los valores desde la clase de constantes:


# Importar la clase CGameConstants
from api.CGameConstants import *

...

# Mover el objeto.
def update(self):

	...

# Disparar con la tecla [Space].
if CKeyboard.inst().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)

Esto que hemos hecho se llama reorganizar el código, para hacerlo más prolijo, y minimizar los posibles errores en el futuro. Pero esto también es fundamental para hacer buenos juegos. Por ejemplo, si queremos probar tener una ventana de diferente tamaño, solo tendremos que cambiar los valores en la clase de constantes, y el juego seguirá funcionando.


De la manera en que estaba antes, deberíamos cambiar los valores en cada sitio del juego donde se usen, y eso lleva tiempo y es probable que nos olvidemos de alguno y luego haya problemas en el juego.

La clave para la flexibilidad que necesitan los juegos es programar limpio, claro y tener en todo momento el cien por ciento del control de lo que hace el programa.


De esta forma, hemos terminado con el manager de balas. En el siguiente capítulo implementaremos el manager de enemigos, para tener muchos enemigos en en juego.





 
 
 

Comments


bottom of page