top of page
Buscar
  • Foto del escritorFernando Sansberro

18. Terminando el Nivel y Puliendo

Actualizado: 24 dic 2021

Llegamos a la parte más linda o divertida de hacer un videojuego, el pulido (polishing en inglés). Esto es, trabajar en los detalles para que el juego sea muy divertido, se vea lindo y se sienta realmente bien.


Durante todo el desarrollo del juego estaremos realizando testing con personas, para ver si les gusta el juego y detectar posibles problemas que iremos solucionando. Atenderemos a las sugerencias que nos hagan nuestros amigos, y eso lleva a realizar un mejor juego.


En esta parte, ya tenemos el juego casi completo, y lo que haremos ahora será terminarlo, atacando los detalles que quedan. Naturalmente que en esta etapa podemos estar mucho tiempo, y podría no acabar nunca. Como se trata de un ejemplo para un libro, haremos pocas tareas. Tu eres libre de seguir este juego o crear el tuyo propio con todas las características que te sientas capaz de realizar.


A continuación arreglaremos varios detalles que tiene el juego, antes de terminarlo.


Matar la Bala de los Enemigos al Pegarle al Jugador


Si corremos el último ejemplo, vemos que cuando una bala enemiga le pega al jugador, lo mata, pero la bala continúa su camino. Lo que haremos es hacer que cualquier bala que le pegue al jugador, se muera. No importa en qué estado esté el jugador, la bala se morirá. El jugador, pasa al estado explotando solamente si lo toca una bala y se encuentra en el estado normal.


Veamos el ejemplo ubicado en la carpeta capitulo_18\001_matar_bala_enemiga_al_chocar para ver esto implementado y funcionando.


En función update() de la clase CPlayer, hacemos el siguiente cambio:


# Mover el objeto.
def update(self):

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

   # Obtener el enemigo con el cual chocamos.
   enemy = CEnemyManager.inst().collides(self)

   # Si chocamos con un enemigo, el enemigo se muere.
   if enemy != None:
       enemy.hit()

   # Lógica del estado normal.
   if self.getState() == CPlayer.NORMAL:
       if enemy != None:
           self.setState(CPlayer.DYING)
           return
       self.move()

   ...

Como vemos en el código, chequeamos colisiones contra todas las balas fuera de las estructuras if de los estados. De esta forma, la bala se muere, independientemente del estado del jugador. Luego, dentro del estado CPlayer.NORMAL, pasamos al estado CPlayer.DYING si se ha chocado con una bala (esto ocurre cuando la variable enemy no es None, o sea, contiene la referencia al enemigo con el que ha chocado). Repasa la clase CManager si no recuerdas cómo funcionaba esto.


Limitando la Cantidad de Disparos


Al jugar al juego como está ahora, nos damos cuenta que si disparamos mucho, matamos muy fácil a los enemigos. Lo que haremos es hacer que el jugador no pueda disparar más de una bala a la vez (se puede cambiar este valor). Cuando quiera disparar la segunda bala, sonará un sonido indicando que no puede disparar. En el momento que la bala llegue a destino (cuando alcance un enemigo o salga de la pantalla), se podrá disparar otra bala.


Observa el ejemplo ubicado en la carpeta capitulo_18\002_maxima_cantidad_de_balas. Veamos el código de la clase CPlayer, que es donde agregamos código para controlar los disparos:


...

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

   ...

   # Maxima cantidad de disparos a la vez.
   MAX_BULLETS = 1

   ...
   def __init__(self, aType):

       ...

       # Cantidad de balas en el momento.
       self.mBulletCount = 0

       # Estado inicial.
       self.setState(CPlayer.NORMAL)

# Movimiento de la nave.
def move(self):
   ...

   # Disparar.
   if fire:
       if self.mBulletCount < CPlayer.MAX_BULLETS:
           # Incrementar la cantidad de balas vivas.
           self.mBulletCount = self.mBulletCount + 1

           print("FIRE")
           b = CPlayerBullet(self)
           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().add(b)

           # Sonido de disparo.
           CAudioManager.inst().play(CAudioManager.mSoundShootPlayer)
       else:
           # Sonido de que no puede disparar.
           CAudioManager.inst().play(CAudioManager.mSoundCannotShoot)

...

# Invocada cuando una bala del jugador se destruye,
def bulletDestroyed(self):
   # Deecrementar la cantidad de balas vivas.
   self.mBulletCount = self.mBulletCount - 1

Definimos una constante MAX_BULLETS con la cantidad máxima de balas que puede disparar el jugador a la vez. Este valor se pone en 1, y se puede cambiar. La variable self.mBulletCount lleva la cuenta de cuantas balas del jugador hay en la pantalla. Cada vez que se dispara se incrementa esta variable, y no se puede disparar si la cuenta es mayor que MAX_BULLETS. Si se puede disparar, se dispara normalmente y se incrementa la variable. Si no se puede disparar porque se ha alcanzado el máximo de balas, se dispara el sonido indicando que no es posible disparar (por supuesto, habiendo colocado el archivo de sonido en la carpeta de assets). Todo esto se hace en la función move(), en la parte de disparar.


El sonido que agregamos se llama player_cannot_shoot.wav y en la clase CAudioManager lo agregamos (observa el proyecto de ejemplo para copiar el archivo e integrar este sonido).


Luego, cuando la bala se muere, debemos decrementar la cantidad de balas. Si recuerdas (mira el código de move()), cuando se crea la bala se le pasa como parámetro la referencia al jugador que la dispara. Esto era, para saber a quién asignarle el puntaje cuando la bala mataba un enemigo. Pues bien, ahora lo necesitaremos para saber a qué jugador decrementarle la cantidad de balas cuando la bala se destruye.


En la clase CPlayerBullet, cuando la bala es destruida, invoca a la función bulletDestroyed() de la clase CPlayer, y en esta función se decrementa la cuenta de balas, como se ve arriba. A continuación vemos en la clase CPlayerBullet donde se invoca la función al destruirse la bala:


# Liberar lo que haya creado el objeto.
def destroy(self):
   # Invocar esta función para decrementar la cuenta de balas vivas.
   self.mPlayer.bulletDestroyed()

   CSprite.destroy(self) 

Esta forma de comunicarse entre objetos, es clave a la hora de programar videojuegos. Los objetos en un juego constantemente se están mandando mensajes entre sí. En este caso, es como que la bala dice: “he muerto, quien me haya disparado debe saberlo”.


Nota: El envío de mensajes entre objetos del juego puede ser en forma directa como en este caso (invocando a funciones) o a través de un manager. Por ejemplo, que la bala le avise al manager del juego y luego éste le avisa al jugador. Como sea, el resultado es el mismo. Muchas soluciones diferentes implican dónde se coloca el código, pero el funcionamiento en general es el mismo.


La Formación de Enemigos Baja


Lo que haremos a continuación será que la formación de enemigos baje cuando uno de los enemigos toque el borde. Cuando la formación baja, comienza a moverse para el lado contrario. El objetivo en este juego es evitar que las naves enemigas se acerquen al fondo de la pantalla.


Abre y ejecuta el ejemplo ubicado en la carpeta capitulo_18\003_la_formacion_baja. Mira como cuando un enemigo toca el borde, la formación entera baja y comienza a ir hacia el otro lado.


Como la formación contiene a todos los enemigos, y ahora debemos escribir código de comportamiento que es específico para el conjunto de enemigos, colocaremos este código en la clase CEnemyManager, que es la clase que se encarga de los enemigos.


Lo primero que haremos será colocar esta clase en la carpeta game, dado que ya no puede estar en la carpeta api porque tendrá código específico para este juego. Cuando se hace esto hay que cambiar (o agregar) las sentencias import correspondientes, en las otras clases que la usen.


Para controlar la formación, tendremos una variable denominada mDirection que indicará si la formación de enemigos se mueve hacia la derecha o hacia la izquierda (usaremos constantes denominadas RIGHT y LEFT).


En la función update() del manager de enemigos, es donde controlamos que si la formación va hacia la derecha y uno de los enemigos toca el borde derecho, la formación baja y los enemigos comienzan a caminar para el lado contrario. De la misma manera, si la formación va hacia la izquierda, se controla si alguno de los enemigos toca el borde izquierdo. Si esto ocurre, la formación baja y los enemigos comienzan a moverse para el lado contrario.


Veamos el código de CEnemyManager para ver la programación relacionada a la formación de enemigos:


...

# Importar Pygame.
import pygame

from api.CManager import *
from api.CGameConstants import *
from game.CEnemyBullet import *

class CEnemyManager(CManager):

   mInstance = None
   mInitialized = False

   # Constantes para la dirección de la formación de enemigos.
   RIGHT = 0
   LEFT = 1

   # Dirección de la formación.
   mDirection = RIGHT

   ...

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

       # Ver si alguno de los enemigos tocó el borde de la pantalla.
       # Si esto es así, bajar a los enemigos y luego invertir la
       # velocidad de todos los enemigos.
       touched = False
       i = 0
       while i < len(self.mArray):
           if not isinstance(self.mArray[i], CEnemyBullet):

               # Ver si van a la derecha y tocan el borde derecho.
               if CEnemyManager.mDirection == CEnemyManager.RIGHT:
                   if self.mArray[i].getX() + self.mArray[i].getWidth() > CGameConstants.SCREEN_WIDTH:
                       touched = True
                       break
               # Ver si van hacia la izquierda y tocan el borde izq.
               else:
                   if self.mArray[i].getX() < 0:
                       touched = True
                       break

           i = i + 1
       if touched:
           self.invertFormation()

   # 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

   # Bajar los enemigos e invertir las velocidades de todos los enemigos.
   def invertFormation(self):
       if CEnemyManager.mDirection == CEnemyManager.RIGHT:
           CEnemyManager.mDirection = CEnemyManager.LEFT
       else:
           CEnemyManager.mDirection = CEnemyManager.RIGHT

       i = 0
       while i < len(self.mArray):
           if not isinstance(self.mArray[i], CEnemyBullet):
               self.mArray[i].setY(self.mArray[i].getY() + self.mArray[i].getHeight() / 2)
               self.mArray[i].setVelX(self.mArray[i].getVelX() * -1)

           i = i + 1

Cuando un enemigo toca el borde en la dirección que lleva la formación, se invoca a la función invertFormation(). Esta función cambia la variable de dirección según corresponda, y se recorre la lista de enemigos, haciéndolos bajar con la función setY() e invirtiendo su velocidad para que comiencen a moverse para el otro lado, utilizando la función setVelX() y multiplicando la velocidad que tienen por -1. El efecto de multiplicar por -1 es invertir el signo de la velocidad, lo que hace que el enemigo camine para el lado contrario al que se movía.


Nota: Observa en la clase CGameObject en el ejemplo, para ver las funciones setX() y setY() que agregamos a CGameObject. Estas funciones establecen las coordenadas por seeparado (hasta ahora usábamos la función setXY()). También hemos agregado en esta clase las funciones getVelX() y getVelY(). A medida que necesitemos funciones las iremos agregando en las clases de api.


Como nosotros ahora estamos controlando el comportamiento de los enemigos contra el borde de la pantalla, tenemos que hacer que al crearse los enemigos, no se establezca ningún comportamiento de borde (para esto, al crear los enemigos en la clase CLevelState, se utiliza CGameObject.NONE como comportamiento de borde).


Al recorrer la lista del manager de enemigos, debemos recordar que en la lista de enemigos se encuentran tanto las naves enemigas como las balas enemigas. Por esta razón, es que usamos la función isinstance() para saltear a las balas enemigas. Si el enemigo es una instancia de CNave (o sea, es una nave enemiga, de clase CNave), es tenido en cuenta. Si no, es salteada.


Nota: En la clase CEnemyManager hemos eliminado los comentarios de las sentencias import, porque son obvias. Está en cada programador el documentar más o menos. En mi caso, me gusta documentar cada línea, pero si las cosas son muy obvias, no. Elige tu forma de documentar como más te guste.


Nota: Como habrás notado, de a poco vamos cambiando los nombres de las variables y funciones a idioma inglés. Esto es necesario para irnos acostumbrando, ya que se espera en la industria de videojuegos que manejemos el idioma inglés como estándar, para que se pueda trabajar con personas de otras nacionalidades. Obviamente se puede escribir en español, pero a la larga lo tendremos que hacer en inglés porque es una práctica profesional. Es algo para hacerlo con tiempo.


El juego, en este momento queda muy difícil porque la formación de enemigos se mueve rápida y al tocar los bordes baja. Esto lo mejoraremos a continuación.


Velocidad Incremental de los Enemigos


La velocidad de los enemigos comenzará lenta al inicio e irá acelerando a medida que queden menos enemigos en la pantalla. Para esto, tendremos una variable para tener la velocidad mínima (al arrancar el nivel) y la velocidad máxima (cuando queda un solo enemigo). En el medio, se usan valores entre el mínimo y el máximo según la cantidad de enemigos. A esta operación se le llama interpolación, y consiste en establecer un valor entre el mínimo y el máximo, gradualmente, según cierto criterio (en este caso, la cantidad de enemigos que quedan).


Veamos el código del ejemplo ubicado en la carpeta capitulo_18\004_la_formacion_acelera. En la clase CEnemyManager es donde se encuentran los cambios relacionados a la velocidad incremental de la formación de enemigos. Veamos el código.


...

class CEnemyManager(CManager):

   mInstance = None
   mInitialized = False

   # Constantes para la dirección de la formación de enemigos.
   RIGHT = 0
   LEFT = 1

   # Dirección de la formación.
   mDirection = RIGHT

   # Variables para la veelocidad de la formación de enemigos.
   mMinVelX = 0
   mMaxVelX = 0
   mVelX = 0
   mMaxShips = 0

   ...

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

       # Estaablecer la velocidad de la formación según la cantidad de enemigos.
       if self.countShips() != 0:
           # Vale 1 cuando hay un solo enemigo y 0 cuando están
           # todos los enemigos. En el medio se interpola.
           percent = 1 - (float(self.countShips()) / float(self.mMaxShips))
       else:
           percent = 0

       vel = self.mMinVelX + ((self.mMaxVelX - self.mMinVelX) * percent)

       if CEnemyManager.mDirection == CEnemyManager.RIGHT:
           self.setVelX(vel)
       else:
           self.setVelX(-vel)

       # Ver si alguno de los enemigos tocó el borde de la pantalla.
       # Si esto es así, bajar a los enemigos y luego invertir la
       # velocidad de todos los enemigos.
       touched = False
       i = 0
       while i < len(self.mArray):
           if not isinstance(self.mArray[i], CEnemyBullet):

               # Ver si van a la derecha y tocan el borde derecho.
               if CEnemyManager.mDirection == CEnemyManager.RIGHT:
                   if self.mArray[i].getX() + self.mArray[i].getWidth() > CGameConstants.SCREEN_WIDTH:
                       touched = True
                       break
               # Ver si van hacia la izquierda y tocan el borde izq.
               else:
                   if self.mArray[i].getX() < 0:
                       touched = True
                       break

           i = i + 1
       if touched:
           self.invertFormation()

   ...

   # Establecer la velocidad mínima y máxima de la formación.
   def setMinMaxVelX(self, aMinVelX, aMaxVelX):
       self.mMinVelX = aMinVelX
       self.mMaxVelX = aMaxVelX
       self.setVelX(aMinVelX)
       self.mMaxShips = self.countShips()

   # Establecer la velocidad actual de la formación.
   def setVelX(self, aMinVelX):
       self.mVelX = aMinVelX

       i = 0
       while i < len(self.mArray):
           if not isinstance(self.mArray[i], CEnemyBullet):
               self.mArray[i].setVelX(self.mVelX)
           i = i + 1

   # Retorna la cantidad de naves en la formación.
   def countShips(self):
       c = 0
       i = 0
       while i < len(self.mArray):
           if not isinstance(self.mArray[i], CEnemyBullet):
               c = c + 1
           i = i + 1
       return c

Cuando se crean los enemigos, se establece la velocidad mínima y máxima. Esto se hace en la función setMinMaxVelX() que se invoca desde CLevelState al crear los enemigos.


# Función donde se inicializan los elementos necesarios del nivel.
def init(self):
   ...

   # Establecer la velocidad mínima y máxima de la formación.
   CEnemyManager.inst().setMinMaxVelX(1, 8)

Luego, en esta función, se establece la velocidad mínima para todos los enemigos. Para esto, hacemos una función setVelX() en el manager de enemigos, que recibe como parámetro la velocidad, y establece esa velocidad para todos los enemigos (como siempre, usamos la función isinstance() para aplicarlo solo a los objetos de clase CNave y no a las balas enemigas).


En la función update(), calculamos (interpolamos o proyectamos) un número entre cero y uno, según el rango:


1.0 o 100%: Cuando hay un solo enemigo.

0.0 o 0%: Cuando están todos los enemigos.


Y en el medio, todos los valores según cuantos enemigos queden en la pantalla.

La cuenta que hacemos es sumarle a la velocidad mínima, la diferencia entre la velocidad máxima y la velocidad mínima, interpolada por la cantidad de enemigos. Esta operación da la velocidad mínima cuando están todos los enemigos y la velocidad máxima cuando queda un solo enemigo. Para hacer esto, como se ve en el código, se suma a la velocidad mínima, la diferencia entre la velocidad máxima y la mínima, multiplicado por el porcentaje.


Nota: En la operación de porcentaje convertimos los números enteros a flotantes, porque sino la división entre enteros se toma como división entera (lo cual da solamente cero o uno en este caso). Como necesitamos un número entre 0.0 y 1.0, utilizamos números flotantes.


Por último, como ahora estamos estableciendo la velocidad de los enemigos en cada frame, en el estado CNave.EXPLODING de la nave, en la función update() de la clase CNave, le ponemos la velocidad en cero para que no se mueva.


# Mover el objeto.
def update(self):

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

   if self.getState() == CNave.NORMAL:
       self.controlFire()

   elif self.getState() == CNave.EXPLODING:

       # Como en update() del manager se establece velocidad, detenerlo aquí.
       self.stopMove()
      
       if self.isEnded():
           self.die()
           return

También en este ejemplo le restamos un poco a lo que baja la nave para que no quede muy difícil el juego.


Nota: Mientras hacemos el juego, iremos modificando los valores (llamados parámetros) de jugabilidad. Esto hará que el juego resulte más difícil o más fácil. A esta tarea se le llama gameplay balance y es una tarea que hace el diseñador del juego (con ayuda del programador para indicarle dónde se encuentran los valores a cambiar)..


Ocultar o Mostrar el Mouse


En esta sección, incluiremos un par de mejoras simples al juego. La primera mejora es hacer que los jugadores no se vayan de la pantalla. Para eso, en la función init() de CLevelState, al crear los jugadores, se le pasa el comportamiento de borde CGameObject.STOP. Si recuerdas en código en CGameObject, este valor hacía que el jugador no se pase de los bordes que definimos con la función setBounds(). Si no recuerdas su uso, mira la clase CLevelState en el ejemplo que se encuentra en la carpeta capitulo_18\005_ocultar_o_mostrar_mouse.


La segunda mejora consiste en tener una función para no mostrar el sprite del puntero del mouse, que molesta durante el juego. Como el puntero del mouse se maneja en la clase CGame, ahí es donde colocaremos el código. Mira los cambios realizados en esta clase, relacionados a mostrar u ocultar el sprite del puntero del mouse.


...

class CGame(object):

   mInstance = None
   mInitialized = False

   mScreen = None
   mClock = None
   mSalir = False
   mMousePointer = None

   # Variable para mostrar o no el puntero del mouse.
   mShowGamePointer = False

   ...

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

       ...

       self.mShowGamePointer = False
       self.showGamePointer(False)

       ...

   ...

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

       ...

       # Actualizar el sprite del puntero del mouse.
       if (CGame.mMousePointer != None):
           CGame.mMousePointer.update()

       ...

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

       ...

       # Dibujar el puntero del mouse.
       if (CGame.mMousePointer != None):
           CGame.mMousePointer.render(self.mScreen)

       ...

   ...

   def destroy(self):
       ...

       # Destruir el puntero del mouse.
       if (CGame.mMousePointer != None):
           CGame.mMousePointer.destroy()
           CGame.mMousePointer = None
       pygame.mouse.set_visible(True)

       ...

   def showGamePointer(self, aShowGamePointer):
       self.mShowGamePointer = aShowGamePointer

       if (aShowGamePointer):
           if (CGame.mMousePointer == None):
               # Crear el sprite del puntero del mouse.
               CGame.mMousePointer = CMousePointer()
           # Ocultar el puntero del sistema.
           pygame.mouse.set_visible(False)
       else:
           # Eliminar el sprite del puntero del mouse.
           if (CGame.mMousePointer != None):
               CGame.mMousePointer.destroy()
               CGame.mMousePointer = None
           # Mostrar el puntero del sistema.
           pygame.mouse.set_visible(True)

La variable self.mShowGamePointer indica si se está usando un cursor propio (un sprite) o se está usando el puntero del sistema. Para indicar si se usa un sprite o el cursor del sistema, se utiliza la función showGamePointer(), que recibe como parámetro True si se usa un sprite y False si se usa el cursor del sistema.


Esta función según el parámetro, pone activo o no el cursor del sistema, y en caso que se muestre el sprite del puntero del mouse, lo crea. Si ya existiera y se pasa al puntero del sistema, lo destruye.


Luego, en el loop principal, se agrega un chequeo de que la referencia del sprite del puntero del mouse no sea None, al dibujarlo y al actualizarlo (en render() y en update()). Recuerda que si el sprite del puntero del mouse no existe, invocar a una función da error si la referencia es None (no existe). Por este motivo es que se chequea que el sprite exista.


En el menú principal, en la función init() de la clase CMenuState, se invoca a CGame.inst().showGamePointer(True) para mostrar el sprite del puntero del mouse, y en la clase del nivel, CLevelState, en la función init(), se invoca con False para ocultarlo.


Máquina de Estados del Nivel


Cuando arrancamos el juego, el mismo comienza en forma inmediata, lo cual no da tiempo a prepararse y podemos perder una vida. De la misma manera, cuando perdemos todas las vidas, el juego termina abruptamente y no sabemos qué ha pasado. Vamos a corregir estos detalles.


Lo que haremos para que el juego quede más lindo, es agregar un mensaje que diga “Nivel 1” al inicio del juego, que durará unos segundos. Mientras esté el mensaje en pantalla, los enemigos no van a disparar.

De la misma manera, cuando perdemos todas las vidas, aparecerá por unos segundos un texto que dice “Game Over” en el centro de la pantalla y luego pasará al menú principal. Por último, cuando matamos a todos los enemigos, esperaremos unos segundos antes de comenzar el siguiente nivel.



Figura 18-1: Los mensajes de inicio del nivel y de fin del juuego.



Cada uno de estos agregados corresponde a estados en el juego. Haremos una máquina de estados a nivel del juego (en la clase CLevelState), al igual que como la hicimos con el jugador, o los enemigos. Miremos el código de la clase CLevelState en el ejemplo que se encuentra ubicado en la carpeta capitulo_18\006_maquina_de_estados_del_nivel. Ejecuta el juego para ver cómo funciona el nivel con estos estados.


Como es la primera vez que ponemos una máquina de estados en una clase de una pantalla, examinemos todo el código, porque debemos de de hacer lo mismo que hicimos con los objetos (manejar los estados y colocar las funciones setState(), getState() y getTimeState() tanto en la clase como en la clase base). Al igual que hicimos, por ejemplo, con CPlayer y CGameObject, para que todos los objetos del juego tengan máquina de estados, ahora haremos una máquina de estados en CLevelState y en CGameState para que todas las pantallas lo tengan disponible.


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

#--------------------------------------------------------------------
# Clase CLevelState.
# Nivel del juego. Es el juego en sí.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------

import pygame
from api.CGameState import *
from api.CGame import *
from game.CNave import *
from game.CPlayer import *
from api.CGameObject import *
from game.CGameData import *
from api.CGameConstants import *
from api.CTextSprite import *
from game.CEnemyManager import *

class CLevelState(CGameState):

   mImgSpace = None
   mPlayer1 = None
   mPlayer2 = None

   mTextLives1 = None
   mTextLives2 = None
   mTextScore1 = None
   mTextScore2 = None

   # Máquina de estados.
   PLAYING = 0
   INIT_LEVEL = 1
   TRANSITION = 2
   GAME_OVER = 3

   # Variables para el control de los mensajes y estados.
   mLevel = 1
   TIME_SHOWING_LEVEL_TEXT = CGame.FPS * 2
   TIME_TRANSITION = CGame.FPS * 1
   TIME_SHOWING_GAME_OVER = CGame.FPS * 4
   mText = None

   def __init__(self):
       CGameState.__init__(self)
       print("LevelState constructor")

       self.mImgSpace = None
       self.mPlayer1 = None
       self.mPlayer2 = None

       self.mTextScore1 = None
       self.mTextScore2 = None
       self.mTextLives1 = None
       self.mTextLives2 = None

       # Comienzo del juego.
       self.mLevel = 1
       self.mText = None

   # Función donde se inicializan los elementos necesarios del nivel.
   def init(self):
       CGameState.init(self)
       print("LevelState init")

       # Cargar la imagen del fondo. La imagen es de 640 x 360.
       self.mImgSpace = pygame.image.load("assets/images/space_640x360.jpg")
       self.mImgSpace = self.mImgSpace.convert()
  
       # Dibujar la imagen cargada en la imagen de background.
       CGame.inst().setBackground(self.mImgSpace)

       # Crear la formación inicial.
       self.initEnemies()

       # Crear el jugador 1.
       self.mPlayer1 = CPlayer(CPlayer.TYPE_PLAYER_1)
       self.mPlayer1.setXY(CGame.SCREEN_WIDTH / 4 - self.mPlayer1.getWidth() / 2, CGame.SCREEN_HEIGHT - self.mPlayer1.getHeight() - 20)
       self.mPlayer1.setBounds(0, 0, CGame.SCREEN_WIDTH - self.mPlayer1.getWidth(), CGame.SCREEN_HEIGHT)
       self.mPlayer1.setBoundAction(CGameObject.STOP)

       # Crear el jugador 2.
       self.mPlayer2 = CPlayer(CPlayer.TYPE_PLAYER_2)
       self.mPlayer2.setXY(CGame.SCREEN_WIDTH / 4 * 3 - self.mPlayer2.getWidth() / 2, CGame.SCREEN_HEIGHT - self.mPlayer2.getHeight() - 20)
       self.mPlayer2.setBounds(0, 0, CGame.SCREEN_WIDTH - self.mPlayer2.getWidth(), CGame.SCREEN_HEIGHT)
       self.mPlayer2.setBoundAction(CGameObject.STOP)

       # Ejecutar la música de background (loop) del juego.
       pygame.mixer.music.load("assets/audio/music_game.ogg")
       pygame.mixer.music.play(-1)
       # Poner el volumen de la música.
       pygame.mixer.music.set_volume(0.5)

       # Inicializar los datos del juego.
       CGameData.inst().setScore1(0)
       CGameData.inst().setLives1(3)
       CGameData.inst().setScore2(0)
       CGameData.inst().setLives2(3)

       self.mTextScore1 = CTextSprite("SCORE: " + str(CGameData.inst().getScore1()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
       self.mTextScore1.setXY(5, 5)
       self.mTextLives1 = CTextSprite("VIDAS: " + str(CGameData.inst().getLives1()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
       self.mTextLives1.setXY(5, CGame.SCREEN_HEIGHT - 20 - 5)

       self.mTextScore2 = CTextSprite("SCORE: " + str(CGameData.inst().getScore2()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
       self.mTextScore2.setXY(530, 5)
       self.mTextLives2 = CTextSprite("VIDAS: " + str(CGameData.inst().getLives2()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
       self.mTextLives2.setXY(540, CGame.SCREEN_HEIGHT - 20 - 5)

       # Ocultar el sprite del puntero del mouse.
       CGame.inst().showGamePointer(False)

       # Establecer el estado inicial.
       self.setState(CLevelState.INIT_LEVEL)

   # Actualizar los objetos del nivel.
   def update(self):
       CGameState.update(self)
       print("LevelState update()")

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

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

       # Lógica de los jugadores.
       self.mPlayer1.update()
       self.mPlayer2.update()

       if self.getState() == CLevelState.INIT_LEVEL:
           if self.getTimeState() > CLevelState.TIME_SHOWING_LEVEL_TEXT:
               self.setState(CLevelState.PLAYING)
               return

       elif self.getState() == CLevelState.PLAYING:
           if self.mPlayer1.isGameOver() and self.mPlayer2.isGameOver():
               print("AMBOS JUGADORES MUEREN - LOSE CONDITION")
               self.setState(CLevelState.GAME_OVER)
               return

           if CEnemyManager.inst().getLength() == 0:
               print("TODOS LOS ENEMIGOS MUEREN - WIN CONDITION")
               self.setState(CLevelState.TRANSITION)
               return

       elif self.getState() == CLevelState.GAME_OVER:
           if self.getTimeState() > CLevelState.TIME_SHOWING_GAME_OVER:
               from game.states.CMenuState import CMenuState
               nextState = CMenuState()
               CGame.inst().setState(nextState)
		    return

       elif self.getState() == CLevelState.TRANSITION:
           if self.getTimeState() > CLevelState.TIME_TRANSITION:
               self.nextLevel()
               return

       self.mTextScore1.update()
       self.mTextScore2.update()
       self.mTextLives1.update()
       self.mTextLives2.update()

       if self.mText != None:
           self.mText.update()

   # Dibujar el frame del nivel.
   def render(self):
       CGameState.render(self)
       print("LevelState render()")

       # Obtener la referencia a la pantalla.
       screen = CGame.inst().getScreen()

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

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

       # Dibujar los jugadores.
       self.mPlayer1.render(screen)
       self.mPlayer2.render(screen)

       # Dibujar el texto del score y las vidas.
       self.mTextScore1.setText("SCORE: " + str(CGameData.inst().getScore1()))
       self.mTextLives1.setText("VIDAS: " + str(CGameData.inst().getLives1()))
       self.mTextScore2.setText("SCORE: " + str(CGameData.inst().getScore2()))
       self.mTextLives2.setText("VIDAS: " + str(CGameData.inst().getLives2()))

       self.mTextScore1.render(screen)
       self.mTextLives1.render(screen)
       self.mTextScore2.render(screen)
       self.mTextLives2.render(screen)

       if self.mText != None:
           self.mText.render(screen)

   # Destruir los objetos creados en el nivel.
   def destroy(self):
       CGameState.destroy(self)
       print("LevelState destroy()")

       # Destruir la nave de los jugadores.
       self.mPlayer1.destroy()
       self.mPlayer1 = None
       self.mPlayer2.destroy()
       self.mPlayer2 = None

       self.mImgSpace = None

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

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

       self.mTextScore1.destroy()
       self.mTextScore1 = None
       self.mTextLives1.destroy()
       self.mTextLives1 = None
       self.mTextScore2.destroy()
       self.mTextScore2 = None
       self.mTextLives2.destroy()
       self.mTextLives2 = None

       if self.mText != None:
           self.mText.destroy()
		self.mText = None

       CGameData.inst().destroy()

   # Establece el estado actual e inicializa
   # las variables correspondientes al estado.
   def setState(self, aState):
       CGameState.setState(self, aState)

       if self.getState() == CLevelState.INIT_LEVEL:
           CEnemyManager.inst().setCanShoot(False)
           self.mText = CTextSprite("NIVEL " + str(self.mLevel), 60, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
           		self.mText.setXY((CGame.SCREEN_WIDTH - self.mText.getWidth()) / 2, (CGame.SCREEN_HEIGHT - self.mText.getHeight()) / 2)
       elif self.getState() == CLevelState.TRANSITION:
           pass
       elif self.getState() == CLevelState.PLAYING:
           CEnemyManager.inst().setCanShoot(True)
       elif self.getState() == CLevelState.GAME_OVER:
           self.mText = CTextSprite("GAME OVER", 80, "assets/fonts/days.otf", (0xFF, 0x00, 0x00))
           self.mText.setXY((CGame.SCREEN_WIDTH - self.mText.getWidth()) / 2, 250)

   # Crear la formación inicial.
   def initEnemies(self):
       f = 0
       while f <= 4:
           c = 0
           while c <= 4:
               n = CNave(f)
               n.setXY(100 + (70 * c), 30 + (35 * f))
               n.setVelX(4)
               n.setVelY(0)
               n.setBounds(0, 0, CGame.SCREEN_WIDTH - n.getWidth(), CGame.SCREEN_HEIGHT - n.getHeight())
               n.setBoundAction(CGameObject.NONE)
		    n.setCanShoot(False)
               CEnemyManager.inst().add(n)
               c = c + 1
           f = f + 1

       # Establecer la velocidad mínima y máxima de la formación.
       CEnemyManager.inst().setMinMaxVelX(1, 8)

   # Función para pasar de nivel.
   def nextLevel(self):
       self.mLevel = self.mLevel + 1
       self.initEnemies()
       self.setState(CLevelState.INIT_LEVEL)

Como siempre hacemos cuando creamos una máquina de estados, definimos las constantes para cada estado, implementamos la función setState() que inicializa cada estado, y se colocan las sentencias if correspondientes a los estados en las funciones update(), render(), etc. Mira el código para ver los cambios relacionados a la máquina de estados.


Luego definimos las constantes de tiempo para cuando se pasa de un estado a otro (mTimeState) y las funciones setState(), getState() y getTimeState() en la clase base (CGameState), de la misma forma que hicimos en CGameobject para los objetos para que todos hereden la máquina de estados, en este caso es para las pantallas.


Veamos ahora como queda la clase CGameState con las funciones de la máquina de estados que se han agregado:


...

class CGameState(object):

   def __init__(self):

       # Estado actual.
       self.mState = 0

       # Control del tiempo en el estado actual.
       self.mTimeState = 0

   def init(self):
       pass

   def update(self):
       # Incrementar el tiempo del estado actual.
       self.mTimeState = self.mTimeState + 1
      
   def render(self):
       pass
      
   def destroy(self):
       pass

   # Retorna el estado actual.
   def getState(self):
       return self.mState

   # Establece el estado actual.
   def setState(self, aState):
       self.mState = aState
       # Resetear el tiempo del estado actual.
       self.mTimeState = 0

   # Retorna el tiempo en el estado actual.
   def getTimeState(self):
       return self.mTimeState

Volvamos a ver el código de CLevelState y veamos los otros cambios realizados. .

En la función init(), se inicializan los enemigos invocando a la función initEnemies() y luego se establece el estado inicial invocando a setState(). El código de InitEnemies() es el mismo que había antes, pero se ha separado en una función por si se quiere usar el nivel como parámetro de velocidad, para hacer el juego más difícil en cada nivel.


Hemos puesto toda la inicialización de los enemigos en una función, llamada initEnemies(), porque cada vez que inicia el nivel hay que crear todos los enemigos de nuevo. Antes lo hacíamos solo al inicio, pero el juego no estaba completo, dado que si los matábamos a todos quedaba el juego sin poder continuar. Ahora, hay una función denominada nextLevel() que incrementa el nivel actual e inicializa los enemigos llamando a la función initEnemies() y pasando al estado CLevelState.INIT_LEVEL.


Nota: Al pasar de estados según el tiempo transcurrido, estamos asumiendo que el framerate está puesto fijo (a 60 FPS). Dicho de otra manera, estamos usando frames para medir el tiempo. Esto no es lo mejor. Deberíamos usar el tiempo real por si la máquina es muy rápida y queremos dibujar más fluido, pero es aceptable usar tiempo basado en frames mientras se aprende a programar juegos, porque simplifica los cálculos.


En CGame hay una constante FPS para tener los frames por segundo al que corre el juego. Este valor lo necesitamos para indicar cuántos cuadros corresponde un segundo al poner los textos en el juego.


Otro cambio que necesitamos hacer en CLevelState es que los enemigos no disparen cuando está comenzando el nivel. Para eso, se hace una función canShoot() en el manager de enemigos (CEnemyManager), y se le pasa como parámetro True si pueden disparar y False si no pueden. Este valor se establece en la función setState() según el estado del juego. Al inicio no pueden disparar, y cuando se pasa al estado CLevelState.PLAYING, pueden disparar.


La función canShoot() del manager de enemigos recorre todos los enemigos e invoca a la función canShoot() del enemigo para indicarle si puede disparar o no. Al crear el enemigo se lo crea invocando a la sentencia n.setCanShoot(False) así no dispara. Luego, al pasar al estado CLevelState.PLAYING, se habilita el disparo.


También para mostrar u ocultar el texto según el estado del nivel, invocamos a la función setVisible() con True o False según el caso. Recuerda que CTextSprite hereda de CSprite, que tiene este comportamiento de visible/invisible.


Nota: Este y otros cambios hechos en el código del libro, ya no se van a listar todos. Debes revisar las variables y funciones relacionadas, y ser capaz de incorporarlas a tu propio proyecto. Si algo no sale bien, siempre tienes la opción de partir desde el proyecto del libro, o consultar las diferencias entre tu proyecto y el código del libro con un profesor. De esta forma, ya iremos acelerando en el desarrollo, para hacer juegos más complejos. Recuerda que la práctica es lo que hace bueno a un programador.


Animación de la Llama (Usar un Sprite dentro de Otro)


En esta sección le agregaremos una llama a la nave del jugador. Para esto, crearemos un sprite animado con la llama, y lo colocaremos en la parte inferior de la nave. Tendremos un sprite que es usado dentro de otro sprite.

Figura 18-2: Fuego de la nave del jugador.



Podríamos hacer que el fuego sea parte del sprite animado, pero como la nave gira, es mejor hacerlo de esta manera, y además aprenderemos cómo se puede componer un sprites de otros sprites.


Mira el ejemplo que se encuentra en la carpeta capitulo_18\007_llama_de_los_jugadores y ejecútalo. Verás que las naves de los jugadores tienen una llama abajo.


Comenzamos creando un sprite animado para la llama, que implementamos en la clase Flame. Como siempre, ponemos las imágenes de la llama en la carpeta de imágenes en la carpeta de assets, y cargamos los frames al inicio. La animación de la llama es cíclica, por lo cual la clase no tiene mucho código.


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

# -------------------------------------------------------------------
# Clase CFlame.
# Fuego de la nave del jugador.
#
# Autor: Fernando Sansberro - Batovi Games.
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
# -------------------------------------------------------------------

import pygame
from api.CAnimatedSprite import *

class CFlame(CAnimatedSprite):

   def __init__(self):

       CAnimatedSprite.__init__(self)

       self.mFrames = []
       i = 0
       while i <= 2:
           num = "0" + str(i)
           tmpImg = pygame.image.load("assets/images/fire" + num + ".png")
           tmpImg = tmpImg.convert_alpha()
           self.mFrames.append(tmpImg)
           i = i + 1

       self.initAnimation(self.mFrames, 0, 2, True)

   def update(self):
       CAnimatedSprite.update(self)

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

   def destroy(self):
       CAnimatedSprite.destroy(self)

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

En la clase CPlayer, es donde colocaremos la llama del jugador. En el constructor creamos la instancia de la llama y luego en las funciones update() y render() invocamos a las funciones update() y render() de la llama respectivamente. En cada llamada a la función update() de la nave del jugador, se invoca a la función setFlamePosition(), que coloca al sprite de la llama centrado en la parte inferior de la nave. De esta forma, cada vez que se mueve la nave, la llama lo sigue. Esta es la forma de implementar un sprite compuesto, o sea, un sprite que contiene otros sprites.

Vemos en el código de la clase CPlayer los cambios que tienen que ver con la llama.


...

from game.CFlame import *

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

   ...

   def __init__(self, aType):

       ...

       # Crear la llama de la nave y posicionarla.
       self.mFlame = CFlame()
       self.setFlamePosition()

       # Estado inicial.
       self.setState(CPlayer.NORMAL)

   # Mover el objeto.
   def update(self):

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

       # Luego de mover la nave, actualizar la llama.
       self.mFlame.update()
       self.setFlamePosition()

       # Obtener el enemigo con el cual chocamos.
       enemy = CEnemyManager.inst().collides(self)

       ...

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

       # En el estado game over la nave no se  dibuja.
       if self.getState() == CPlayer.GAME_OVER:
           return

       # Poner visible o invisible según el caso.
       if self.getState() == CPlayer.DYING:
           if self.getTimeState() % 8 == 0:
               self.setVisible(True)
           else:
               self.setVisible(False)

       elif self.getState() == CPlayer.EXPLODING:
           pass

       elif self.getState() == CPlayer.START:
           if self.getTimeState() % 16 == 0:
               self.setVisible(True)
           else:
               self.setVisible(False)

       CAnimatedSprite.render(self, aScreen)

       # Dibujar la llama.
       self.mFlame.render(aScreen)

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

       ...

       if self.mFlame != None:
           self.mFlame.destroy()
           self.mFlame = None

   # Establece el estado actual e inicializa
   # las variables correspondientes al estado.
   def setState(self, aState):
       CAnimatedSprite.setState(self, aState)

       # Por defecto poner el sprite visible.
       self.setVisible(True)

       if self.getState() == CPlayer.NORMAL:
           self.initAnimation(self.mFrames, 0, 0, False)
           self.gotoAndStop(0)

       elif self.getState() == CPlayer.DYING:
           self.stopMove()
           self.initAnimation(self.mFrames, 0, 0, False)
           self.gotoAndStop(0)

           # Ejecutar sonido de morir.
           CAudioManager.inst().play(CAudioManager.mSoundHitPlayer)

       elif self.getState() == CPlayer.EXPLODING:
           self.initAnimation(self.mFramesExplosion, 0, 2, False)

           # Ejecutar el sonido de la explosión.
           CAudioManager.inst().play(CAudioManager.mSoundExplosionPlayer)

           self.mFlame.setVisible(False)

       elif self.getState() == CPlayer.START:
           self.initAnimation(self.mFrames, 0, 0, False)
           self.gotoAndStop(0)

       elif self.getState() == CPlayer.GAME_OVER:
           self.setVisible(False)

   ...

   # Establece si el sprite es visible o no.
   # Parámetro: True para que se dibuje, False para que no se dibuje.
   def setVisible(self, aIsVisible):

       # Invocar destroy() de CAnimatedSprite.
       CAnimatedSprite.setVisible(self, aIsVisible)

       # Mostrar u ocultar la llama.
       self.mFlame.setVisible(aIsVisible)

   # Posicionar la llama debajo de la nave.
   def setFlamePosition(self):
       self.mFlame.setX(self.getX() + self.getWidth() / 2 - self.mFlame.getWidth() / 2)
       self.mFlame.setY(self.getY() + self.getHeight())

En la función update() del jugador, se coloca la llama en su posición, luego de que se haya movido la nave, o sea, luego de invocar a la función update() de la clase base que es la que la coloca en su nueva posición. Esto es para que la llama se coloque usando la posición del frame actual. Es un error común colocar la llama antes de mover la nave, con lo cual quedarían desfasadas.


En la función setState(), se coloca la llama visible o invisible, según corresponda el estado. Por defecto la nave siempre está visible, por eso se llama a setVisible(True) al inicio de setState(). Hay estados en los cuales la nave parpadea, y lo mismo hay que hacer con la llama. Esto lo hacemos en una propia función setVisible() en la clase, la cual pone la visibilidad de la nave, y de todos sus componentes (en este caso, la llama).


# Establece si el sprite es visible o no.
   # Parámetro: True para que se dibuje, False para que no se dibuje.
   def setVisible(self, aIsVisible):

       # Invocar destroy() de CAnimatedSprite.
       CAnimatedSprite.setVisible(self, aIsVisible)

       # Mostrar u ocultar la llama.
       self.mFlame.setVisible(aIsVisible)

Nota: Este concepto es muy importante al componer sprites. Se sobreescriben las funciones de la clase base, pero se agrega la parte para los componentes del sprite.


Luego, por último, se pone invisible la llama cuando explota la nave (en el estado CPlayer.EXPLODING).


Modo de Uno o Dos Jugadores

Para terminar con el nivel del juego, haremos que se pueda jugar con uno o con dos jugadores. Para eso, cambiaremos el mensaje que se muestra en el menú principal, por el mensaje: "Pulsa: [Space] = Un jugador, [Q] = Dos jugadores...".


Cuando se pulsa [Space] o [Q] para seleccionar uno o dos jugadores respectivamente, se le pasa como parámetro al constructor de la clase CLevelState el número de jugadores (1 o 2).


Mira el ejemplo que se encuentra en la carpeta capitulo_18\008_uno_o_dos_jugadores. En la clase CMenuState, se invoca al constructor de CLevelState con la cantidad de jugadores como parámetro:


def update(self):
   CGameState.update(self)

   if CKeyboard.inst().fire():
       from game.states.CLevelState import CLevelState
       nextState = CLevelState(1)
       CGame.inst().setState(nextState)
       return

   if CKeyboard.inst().fire2():
       from game.states.CLevelState import CLevelState
       nextState = CLevelState(2)
       CGame.inst().setState(nextState)
       return

   self.mTextTitle.update()
   self.mTextPressFire.update()

Luego, en la clase CLevelState, tenemos una variable mNumPlayers donde guardamos la cantidad de jugadores que hay en el juego (1 o 2). Según esta variable, se crea o no el segundo jugador, y luego se actualiza o no, se dibuja o no, o se muestra o no los indicadores del segundo jugador. También se usa esta variable para tener en cuenta uno o dos jugadores a la hora de morir y chequear la condición de perder. Veamos el código de CLevelState con los cambios:


...

class CLevelState(CGameState):

   ...

   # Cantidad de jugadores.
   mNumPlayers = 1

   def __init__(self, aNumPlayers):
       CGameState.__init__(self)
       print("LevelState constructor")

       # Establecer la cantidad de jugadores.
       self.mNumPlayers = aNumPlayers

       self.mImgSpace = None
       self.mPlayer1 = None
       self.mPlayer2 = None

       self.mTextScore1 = None
       self.mTextScore2 = None
       if self.mNumPlayers == 2:
           self.mTextLives1 = None
           self.mTextLives2 = None

       # Comienzo del juego.
       self.mLevel = 1
       self.mText = None

   # Función donde se inicializan los elementos necesarios del nivel.
   def init(self):
       ...

       # Crear el jugador 1.
       self.mPlayer1 = CPlayer(CPlayer.TYPE_PLAYER_1)
       self.mPlayer1.setXY(CGame.SCREEN_WIDTH / 4 - self.mPlayer1.getWidth() / 2, CGame.SCREEN_HEIGHT - self.mPlayer1.getHeight() - 20)
       self.mPlayer1.setBounds(0, 0, CGame.SCREEN_WIDTH - self.mPlayer1.getWidth(), CGame.SCREEN_HEIGHT)
       self.mPlayer1.setBoundAction(CGameObject.STOP)

       if self.mNumPlayers == 2:
           # Crear el jugador 2.
           self.mPlayer2 = CPlayer(CPlayer.TYPE_PLAYER_2)
           self.mPlayer2.setXY(CGame.SCREEN_WIDTH / 4 * 3 - self.mPlayer2.getWidth() / 2, CGame.SCREEN_HEIGHT - self.mPlayer2.getHeight() - 20)
           self.mPlayer2.setBounds(0, 0, CGame.SCREEN_WIDTH - self.mPlayer2.getWidth(), CGame.SCREEN_HEIGHT)
           self.mPlayer2.setBoundAction(CGameObject.STOP)

       ...

       self.mTextScore1 = CTextSprite("SCORE: " + str(CGameData.inst().getScore1()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
       self.mTextScore1.setXY(5, 5)
       self.mTextLives1 = CTextSprite("VIDAS: " + str(CGameData.inst().getLives1()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
       self.mTextLives1.setXY(5, CGame.SCREEN_HEIGHT - 20 - 5)

       if self.mNumPlayers == 2:
           self.mTextScore2 = CTextSprite("SCORE: " + str(CGameData.inst().getScore2()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
           self.mTextScore2.setXY(530, 5)
           self.mTextLives2 = CTextSprite("VIDAS: " + str(CGameData.inst().getLives2()), 20, "assets/fonts/days.otf", (0xFF, 0xFF, 0xFF))
           self.mTextLives2.setXY(540, CGame.SCREEN_HEIGHT - 20 - 5)

       ...

   # Actualizar los objetos del nivel.
   def update(self):
       CGameState.update(self)

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

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

       # Lógica de los jugadores.
       self.mPlayer1.update()
       if self.mNumPlayers == 2:
           self.mPlayer2.update()

       print(self.getState())

       if self.getState() == CLevelState.INIT_LEVEL:
           if self.getTimeState() > CLevelState.TIME_SHOWING_LEVEL_TEXT:
               self.setState(CLevelState.PLAYING)
               return

       elif self.getState() == CLevelState.PLAYING:
           if (self.mNumPlayers == 1 and self.mPlayer1.isGameOver()) or (self.mNumPlayers == 2 and self.mPlayer1.isGameOver() and self.mPlayer2.isGameOver()):
               print("AMBOS JUGADORES MUEREN - LOSE CONDITION")
               self.setState(CLevelState.GAME_OVER)
               return

           if CEnemyManager.inst().getLength() == 0:
               print("TODOS LOS ENEMIGOS MUEREN - WIN CONDITION")
               self.setState(CLevelState.TRANSITION)
               return

       ...

       self.mTextScore1.update()
       self.mTextLives1.update()
       if self.mNumPlayers == 2:
           self.mTextScore2.update()
           self.mTextLives2.update()

       if self.mText != None:
           self.mText.update()

   # Dibujar el frame del nivel.
   def render(self):
       CGameState.render(self)

       # Obtener la referencia a la pantalla.
       screen = CGame.inst().getScreen()

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

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

       # Dibujar los jugadores.
       self.mPlayer1.render(screen)
       if self.mNumPlayers == 2:
           self.mPlayer2.render(screen)

       # Dibujar el texto del score y las vidas.
       self.mTextScore1.setText("SCORE: " + str(CGameData.inst().getScore1()))
       self.mTextLives1.setText("VIDAS: " + str(CGameData.inst().getLives1()))
       if self.mNumPlayers == 2:
           self.mTextScore2.setText("SCORE: " + str(CGameData.inst().getScore2()))
           self.mTextLives2.setText("VIDAS: " + str(CGameData.inst().getLives2()))

       self.mTextScore1.render(screen)
       self.mTextLives1.render(screen)
       if self.mNumPlayers == 2:
           self.mTextScore2.render(screen)
           self.mTextLives2.render(screen)

       if self.mText != None:
           self.mText.render(screen)

   # Destruir los objetos creados en el nivel.
   def destroy(self):
       CGameState.destroy(self)

       # Destruir la nave de los jugadores.
       self.mPlayer1.destroy()
       self.mPlayer1 = None
       if self.mNumPlayers == 2:
           self.mPlayer2.destroy()
           self.mPlayer2 = None

       self.mImgSpace = None

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

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

       self.mTextScore1.destroy()
       self.mTextScore1 = None
       self.mTextLives1.destroy()
       self.mTextLives1 = None
       if self.mNumPlayers == 2:
           self.mTextScore2.destroy()
           self.mTextScore2 = None
           self.mTextLives2.destroy()
           self.mTextLives2 = None

       if self.mText != None:
           self.mText.destroy()
           self.mText = None

       CGameData.inst().destroy()

   ...

Como vemos en el código, hay una sentencia if para cada vez que existe código relacionado al segundo jugador. Esto es porque el segundo jugador podría no existir, y si este es el caso, no se debe invocar a sus funciones update(), render() y destroy().Del mismo modo, si hay un solo jugador, entonces no hay que actualizar ni dibujar los sprites de texto correspondientes a los marcadores del segundo jugador.


Cuando se está en el estado CLevelState.PLAYING, hay que preguntar si todos los jugadores han muerto para saber si termina el juego. La condición para terminar el juego se ve reflejada en el siguiente if:


elif self.getState() == CLevelState.PLAYING:
   if (self.mNumPlayers == 1 and self.mPlayer1.isGameOver()) or (self.mNumPlayers == 2 and self.mPlayer1.isGameOver() and self.mPlayer2.isGameOver()):
       print("AMBOS JUGADORES MUEREN - LOSE CONDITION")
       self.setState(CLevelState.GAME_OVER)
       return

Esto es, si la cantidad de jugadores es uno y el primer jugador está muerto, se termina (la sentencia if vale True). O, si la cantidad de jugadores es dos y tanto el jugador uno como el dos han muerto. Analiza bien los paréntesis y lee la frase en español para entender cómo funciona esta pregunta.



Figura 18-3: El juego con un solo jugador.



Con esto terminamos nuestro primer juego. ¡Felicitaciones! Ha sido un largo camino hasta este punto, y mucho de lo que hemos hecho lo reultizaremos en nuestros próximos juegos.


Nota: Aún quedan detalles y muchos temas por aprender. Algunos temas irán en los futuros libros, y otros en forma de artículos que serán publicados en el sitio del libro (www.fsansberro.com). Visita frecuentemente este sitio para ver las novedades.


139 visualizaciones0 comentarios

Entradas Recientes

Ver todo
bottom of page