15. Manejo de Textos y Fuentes
- Fernando Sansberro
- 20 nov 2021
- 18 Min. de lectura
Actualizado: 24 dic 2021
En este capítulo vamos a ver cómo dibujar texto en la pantalla. En Pygame, podemos dibujar textos usando alguna fuente (tipos de letra). Vamos a comenzar mostrando en la pantalla del juego, un texto con las vidas y el puntaje (score) del jugador. En los siguientes capítulos haremos todas las pantallas del juego, como el menú principal, la pantalla de ayuda, la pantalla de créditos, etc. y en todas esas pantallas necesitaremos dibujar texto.
Dibujando un Texto
En Pygame los textos son dibujados en la pantalla como imágenes. Es decir, cuando mostramos texto en la pantalla, lo que estamos haciendo realmente es creando una imagen y dibujándola en la pantalla.
Para mostrar un texto, debemos crear primero un objeto de clase pygame.font.Font, al cual le pasamos como parámetro la fuente a usar (la ruta del archivo de la fuente) y el tamaño de la fuente. Luego usamos la función render() en esa fuente que hemos creado, lo que da como resultado una nueva imagen conteniendo el texto que le pasamos como parámetro dibujado en la imagen. Al final, mostramos esa imagen en la pantalla, normalmente, como hacemos con cualquier imagen.
Vamos a hacer una función para dibujar un texto en la pantalla o en una imagen. A esta función le pasamos varios parámetros. Veamos la función a continuación:
# Función para dibujar texto.
# Parámetros: La pantalla donde dibujar, coordenadas (x,y),
# texto ,tamaño de fuente y color del texto.
def drawText(aScreen, aX, aY, aMsg, aSize, aColor=(0,0,0)):
font = pygame.font.Font("assets/fonts/days.ot", aSize)
imgTxt = font.render(aMsg, True, aColor)
aScreen.blit(imgTxt, (aX, aY))
Esta función recibe como parámetros lo siguiente:
aScreen: Superficie donde va a dibujar el texto. Puede ser la pantalla o cualquier imagen.
aX, aY: Coordenadas en donde dibujar el texto.
aMsg: String (texto) que se quiere mostrar.
aSize: Tamaño de la fuente que se va a usar.
aColor: Color del texto.
En primera línea de la función, se crea un objeto pygame.font.Font, al cual se le pasa como parámetro la fuente a usar y el tamaño de la fuente (el tamaño es similar a cuando elegimos una fuente y tamaño en un editor de texto). Como los archivos de fuentes son assets, hemos hecho una carpeta para las fuentes en el carpeta de assets en donde colocamos las fuentes usadas en el juego (assets/fonts).
La segunda línea usa la función render() de la fuente para crear una imagen a partir del texto que queremos mostrar, utilizando la información de la fuente para generar la imagen. A la función render() se le pasa como segundo parámetro un valor booleano (True o False) que indica si se quiere el texto con antialias (suavizado de los pixeles) (True) o sin antialias (False). El tercer parámetro es el color del texto. Recordar que el color siempre es una tupla con los valores RGB (red, green, blue).
Nota: La función font.render() podría recibir un cuarto parámetro que es el color de fondo. Este parámetro no lo usamos porque sino quedaría un rectángulo de fondo con el color. Usaremos siempre el texto con un fondo transparente.
Nota: El uso de antialias hace que los textos se vean mejor, con más suavizado en los bordes. Si no se usa antialias se ven los píxeles bien marcados en los bordes. En la función usaremos antialias, pero hay que tener en cuenta que es una operación más lenta que dibujar sin antialias. Si son muchos los textos del juego, es algo a tener en cuenta.
Por último, usamos la ya conocida función blit() en la imagen para dibujar la imagen generada con el texto, en las coordenadas indicadas.
Abre y ejecuta el ejemplo que se encuentra en la carpeta capitulo_15\001_mostrar_texto. En el programa main.py, se encuentra implementada la función drawText(), explicada antes, casi al final del programa. Esta función usa la fuente days.otf que se encuentra en la carpeta assets/fonts. Copia esta carpeta a tu propio proyecto.
En la función render() en main.py, mostramos dos textos, el primero para mostrar el puntaje del jugador en la parte superior izquierda de la pantalla, y el segundo para mostrar las vidas, en la parte inferior de la pantalla.
# Dibujar el frame y actualizar la pantalla.
def render():
# Dibujar el fondo.
screen.blit(imgBackground, (0, 0))
# Dibujar los enemigos.
CEnemyManager.inst().render(screen)
# Dibujar los jugadores.
player1.render(screen)
player2.render(screen)
# Dibujar las balas.
CBulletManager.inst().render(screen)
# Dibujar el texto del score y las vidas.
drawText(screen, 5, 5, "SCORE: 00000", 20, (255, 255, 255))
drawText(screen, 5, SCREEN_HEIGHT - 20 - 5, "VIDAS: 3", 20, (255, 255, 255))
# Actualizar la pantalla.
pygame.display.flip()
Dibujamos el score en la parte de arriba de la pantalla y las vidas en la parte inferior.
Recordemos que el segundo y tercer parámetro corresponde a la coordenada x e y donde se va a dibujar el texto. Como al texto le pasamos un alto de 20 puntos, las vidas, el segundo texto, lo dibujamos en la coordenada correspondiente al alto de la pantalla menos el alto del texto. Usamos 5 de margen, tanto arriba como abajo, para que los textos no queden pegados a los bordes de la pantalla.
Los marcadores que se muestran en el juego (puntaje, vidas, nivel, etc.) se denomina HUD (por las siglas de head-up display). Por ahora solamente mostramos un texto fijo, porque recién estamos viendo cómo mostrar textos. Los marcadores del HUD por ahora no estarán funcionales.
Más adelante si perdemos una vida, actualizaremos el contador de vidas en el HUD. Lo mismo cuando matamos a un enemigo, se incrementará el marcador del puntaje.
Al colocar estos textos, vemos que las naves de los jugadores lo tocan. Debemos subir un poco la ubicación de las naves para que no toquen el texto para que queden arriba del texto de las vidas y no lo toque. Como el texto tiene 20 puntos, es la distancia que subimos las naves de los jugadores. En la función init() de main.py restamos la coordenada vertical de las naves y también subimos 20 píxeles las naves enemigas:
# Función de inicialización.
def init():
...
# Crear la formación inicial.
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, SCREEN_WIDTH - n.getWidth(), SCREEN_HEIGHT - n.getHeight())
n.setBoundAction(CGameObject.BOUNCE)
CEnemyManager.inst().add(n)
c = c + 1
f = f + 1
# Crear el jugador 1.
player1 = CPlayer(CPlayer.TYPE_PLAYER_1)
player1.setXY(SCREEN_WIDTH / 4 - player1.getWidth() / 2, SCREEN_HEIGHT - player1.getHeight() - 20)
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() - 20)
player2.setBounds(0, 0, SCREEN_WIDTH - player2.getWidth(), SCREEN_HEIGHT)
...
Al ejecutar el juego, vemos que se muestran los textos del puntaje y de las vidas, y que ni las naves de los jugadores ni las naves enemigas tocan los textos.

Figura 15-1: El juego muestra textos ahora.
Nota: Al crear la fuente en la función drawText(), podríamos pasar como parámetro el nombre de la fuente en lugar del archivo de la fuente, pero como los diferentes sistemas operativos (Windows, Linux, Mac, etc.) tienen diferentes fuentes en el sistema, la fuente usada podría no existir. Por eso, es mejor poner el archivo de fuente en una carpeta de fuentes y distribuirla con el juego (esto es lo que estamos haciendo). Casi siempre vamos a usar un archivo de fuente propio del juego, porque en un juego las fuentes deben de ser lindas o de cierto tema, y las fuentes del sistema generalmente no lo son.
Usando una Fuente del Sistema (System Font)
Pygame nos brinda la función pygame.font.SysFont para crear una fuente del sistema. Esto es, una de las fuentes que trae ya instalado el sistema operativo. Para crear una fuente del sistema, le pasamos como parámetros a la función, el nombre de la fuente y el tamaño. Por ejemplo:
font = pygame.font.SysFont("Comic Sans MS", aSize)
La ventaja de usar una fuente del sistema es que no tenemos que incorporar una fuente con el juego, pero la desventaja es que la fuente podría no existir en el sistema (recuerda que al juego, si tiene éxito, se va a jugar en diferentes plataformas y es importante tener en cuenta este aspecto al desarrollar juegos).
No hay ninguna garantía de que determinada fuente se encuentre en el sistema. Hay pocas fuentes que son compartidas entre diferentes sistemas operativos como Linux, Mac y Windows.
Usando pygame.font.SysFont(), si la fuente no se encuentra en el sistema, Pygame la reemplaza por otra y el programa sigue funcionando (no da error). Si usamos pygame.font.Font() y nos olvidamos de incluir la fuente en el juego, el programa dará error.
Nota: En un juego nuestro, casi siempre utilizaremos una fuente propia, dado que la fuente tiene que ir con la estética del juego. Estas fuentes las colocamos en la carpeta de assets (assets/fonts).
Usando una Fuente Propia del Juego (Custom Font)
En nuestra función drawText(), hemos creado una fuente propia (denominada custom font) usando la función pygame.font.Font y pasándole como parámetro el nombre del archivo de fuente (la ruta del archivo) y el tamaño. Por ejemplo:
font = pygame.font.Font("assets/fonts/days.otf", aSize)
Estos archivos de fuentes se consiguen en sitios especializados en fuentes, o bien se puede crear una fuente nueva si se usa algún editor (software) para hacer una nueva fuente (aunque esto lleva tiempo y no es tan sencillo).
Creando el HUD: Agregando los Puntajes y las Vidas
En este momento ya tenemos el juego funcionando bien y sabemos mostrar texto. Entonces, vamos a agregar el HUD al juego, que muestre el puntaje y las vidas que tienen los jugadores. Recuerda que HUD significa head-up display y se refiere a los marcadores que aparecen sobreimpresos en el juego (vidas, puntos, energía, etc).
Los marcadores le darán al jugador una idea de cómo lo está haciendo en el juego (si va bien o mal). Cuando un juego muestra información de alguna manera (tanto con audio como con gráficos o con texto), se le denomina dar feedback (dar información).
Los jugadores comenzará con 3 vidas cada uno y cada vez que choquemos contra un enemigo (una bala por ejemplo), vamos a perder una vida. Si perdemos todas las vidas el juego termina (más adelante haremos que salga del juego al terminar).
Abre y ejecuta el ejemplo que se encuentra en la carpeta capitulo_15\002_hud_score_y_vidas. Al ejecutar el juego vemos que ahora tenemos en pantalla el puntaje y las vidas de los dos jugadores.
El juego ahora tiene cuatro datos que llevar: los puntajes y las vidas de los dos jugadores. En lugar de utilizar variables en el programa main.py, que haría complicado su seguimiento por todo el código que hay, centralizamos los datos del juego en una clase llamada CGameData, que colocaremos en la carpeta game (porque es algo relacionado al juego).
La clase CGameData será una clase singleton (porque queremos acceder a sus funciones desde cualquier parte del código sin necesitar una instancia). Esta clase tendrá cuatro variables que son las que necesitamos para el juego: mScore1 y mScore2 son el puntaje del primer y segundo jugador respectivamente y mLives1 y mLives2 son las vidas del primer y segundo jugador respectivamente.
La clase CGameData se compone de las variables, más un conjunto de funciones denominadas setters y getters que son funciones para establecer y obtener los valores de cada una de las variables. El código es bastante fácil de entender. Una parte corresponde a implementar el comportamiento del patrón singleton y la otra parte a las funciones para el control de las variables que almacena. Veamos a continuación el código de la clase CGameData:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Clase GameData.
# Clase para manejar los datos del juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
class CGameData(object):
mInstance = None
mInitialized = False
mScore1 = 0
mLives1 = 0
mScore2 = 0
mLives2 = 0
def __new__(self, *args, **kargs):
if (CGameData.mInstance is None):
CGameData.mInstance = object.__new__(self, *args, **kargs)
self.init(CGameData.mInstance)
else:
print("Cuidado: CGameData(): No se debería instanciar más de una vez esta clase. Usar CGameData.inst().")
return CGameData.mInstance
@classmethod
def inst(cls):
if (not cls.mInstance):
return cls()
return cls.mInstance
def init(self):
if (CGameData.mInitialized):
return
CGameData.mInitialized = True
CGameData.mScore1 = 0
CGameData.mLives1 = 0
CGameData.mScore2 = 0
CGameData.mLives2 = 0
def setScore1(self, aScore):
CGameData.mScore1 = aScore
self.controlScores()
def setScore2(self, aScore):
CGameData.mScore2 = aScore
self.controlScores()
def addScore1(self, aScore):
CGameData.mScore1 += aScore
self.controlScores()
def addScore2(self, aScore):
CGameData.mScore2 += aScore
self.controlScores()
def controlScores(self):
# Controlar que los scores no sean negativos o muy grandes.
if (CGameData.mScore1 < 0):
CGameData.mScore1 = 0
if (CGameData.mScore1 > 999999):
CGameData.mScore1 = 999999
if (CGameData.mScore2 < 0):
CGameData.mScore2 = 0
if (CGameData.mScore2 > 999999):
CGameData.mScore2 = 999999
def getScore1(self):
return CGameData.mScore1
def getScore2(self):
return CGameData.mScore2
def setLives1(self, aLives):
CGameData.mLives1 = aLives
self.controlLives()
def setLives2(self, aLives):
CGameData.mLives2 = aLives
self.controlLives()
def addLives1(self, aLives):
CGameData.mLives1 += aLives
self.controlLives()
def addLives2(self, aLives):
CGameData.mLives2 += aLives
self.controlLives()
def controlLives(self):
# Controlar que las vidas no sean negativas o muy grandes.
if (CGameData.mLives1 < 0):
CGameData.mLives1 = 0
if (CGameData.mLives1 > 9):
CGameData.mLives1 = 9
if (CGameData.mLives2 < 0):
CGameData.mLives2 = 0
if (CGameData.mLives2 > 9):
CGameData.mLives2 = 9
def getLives1(self):
return CGameData.mLives1
def getLives2(self):
return CGameData.mLives2
def destroy(self):
CGameData.mInstance = None
Como vemos en el código, la clase CCGameData contiene funciones para establecer los valores de cada una de las variables (setters) y funciones para obtener sus valores (getters). El concepto importante aquí es que las variables (los datos del juego) solamente se modifican mediante estas funciones. Cuando se modifica el puntaje o las vidas, se llama internamente a una función que controla que las vidas estén entre 0 y 9 (esto es arbitrario y se puede cambiar) y que el puntaje esté entre 0 y 999999.
Hacer este control cada vez que modificamos un dato se denomina chequeo de integridad, y con esto nos aseguramos de no cometer errores que son comunes en los juegos amateurs, como tener vidas negativas cuando perdamos muchas vidas, o tener un puntaje negativo, o muy alto que de la vuelta hasta el cero. Cada vez que se modifica el valor de un dato, se chequea su integridad. De esta forma no hay errores.
Una vez que tenemos los datos del juego en la clase CGameData, en el programa main.py los utilizamos. Para eso, importamos la clase, inicializamos los datos usando las funciones setters, y los dibujamos en el HUD utilizando las funciones getters. A continuación se muestra resaltado en negrita los lugares de main.py relacionado con los datos del juego:
...
# Importar Pygame.
import pygame
...
# Importar la clase de los datos del juego.
from game.CGameData import *
...
# Función de inicialización.
def init():
...
# Inicializar los datos del juego.
CGameData.inst().setScore1(0)
CGameData.inst().setLives1(3)
CGameData.inst().setScore2(0)
CGameData.inst().setLives2(3)
...
# Dibujar el frame y actualizar la pantalla.
def render():
# Dibujar el fondo.
screen.blit(imgBackground, (0, 0))
# Dibujar los enemigos.
CEnemyManager.inst().render(screen)
# Dibujar los jugadores.
player1.render(screen)
player2.render(screen)
# Dibujar las balas.
CBulletManager.inst().render(screen)
# Dibujar el texto del score y las vidas.
drawText(screen, 5, 5, "SCORE: " + str(CGameData.inst().getScore1()), 20, (255, 255, 255))
drawText(screen, 5, SCREEN_HEIGHT - 20 - 5, "VIDAS: " + str(CGameData.inst().getLives1()), 20, (255, 255, 255))
drawText(screen, 530, 5, "SCORE: " + str(CGameData.inst().getScore2()), 20, (255, 255, 255))
drawText(screen, 540, SCREEN_HEIGHT - 20 - 5, "VIDAS: " + str(CGameData.inst().getLives2()), 20, (255, 255, 255))
# Actualizar la pantalla.
pygame.display.flip()
...
Como vemos, simplemente usamos CGameData.inst().setScore1() si queremos establecer el puntaje del primer jugador, y usamos CGameData.inst().getScore1() cuando necesitamos obtener su valor para mostrarlo en el HUD. Lo mismo para cada uno de los datos de esa clase.

Figura 15-2: El HUD ahora está mostrándose en pantalla.
Nota: En la clase CGameData podríamos haber implementado una solución mejor para manejar los datos de cada jugador, por ejemplo utilizando un array. Lo hemos hecho con variables por separado para que quede más claro, dado que son pocas variables y funciona bien para nuestro propósito.
Controlando el Puntaje de los Jugadores
En esta sección, haremos que cuando una bala toque a un enemigo, incrementar el puntaje, sumándole el que corresponda a cada enemigo.
Cada vez que matamos a un enemigo, el score aumenta. Haremos que los enemigos de la primera línea valgan 5 puntos, los de la segunda línea valdrán 10 puntos, y así sucesivamente.
Veamos el ejemplo que se encuentra en la carpeta capitulo_15\003_score_funcional. Cuando cada jugador mata a un enemigo, vemos que su score se incrementa.
Para lograr esto, necesitamos saber cuantos puntos nos da cada enemigo. Para esto, pondremos una variable en la clase CSprite, de modo que todos los sprites del juego tengan un score. Es discutible si poner el puntaje en la clase CSprite, en CGameObject o en donde sea (es cuestión de gustos), pero en algún lado lo debemos poner. Lo vamos a hacer en la clase CSprite.
Tendremos una variable mScore que indica cuantos puntos da el sprite al matarlo, y como siempre, tendremos funciones para establecer y obtener su valor.. Veamos el código agregado en la clase CSprite:
...
class CSprite(CGameObject):
# Constructor:
def __init__(self):
CGameObject.__init__(self)
...
# Score del sprite.
self.mScore = 0
...
# Establecer el score del sprite.
def setScore(self, aScore):
self.mScore = aScore
# Obtener el score del sprite.
def getScore(self):
return self.mScore
Ahora, cualquier sprite del juego puede tener un score si lo asignamos con la función setScore(), de lo contrario el score será cero.
Como cada nave ahora tiene su score, en la clase CNave debemos establecerlo. En el constructor de la clase asignamos el score según el tipo de nave:
...
class CNave(CAnimatedSprite):
...
def __init__(self, aType):
CAnimatedSprite.__init__(self)
# Segun el tipo de la nave, la imagen que se carga.
self.mType = aType
if self.mType == CNave.TYPE_PLATINUM:
imgFile = "assets/images/grey_ufo_0"
self.setScore(25)
elif self.mType == CNave.TYPE_GOLD:
imgFile = "assets/images/yellow_ufo_0"
self.setScore(20)
elif self.mType == CNave.TYPE_RED:
imgFile = "assets/images/red_ufo_0"
self.setScore(15)
elif self.mType == CNave.TYPE_CYAN:
imgFile = "assets/images/cyan_ufo_0"
self.setScore(10)
elif self.mType == CNave.TYPE_GREEN:
imgFile = "assets/images/green_ufo_0"
self.setScore(5)
...
En la clase CNave, cambiamos las constantes de las naves para que la nave verde sea la de más abajo de la formación (esto es porque es la que luce más fea).
...
class CNave(CAnimatedSprite):
# Tipos de naves.
TYPE_PLATINUM = 0
TYPE_GOLD = 1
TYPE_RED = 2
TYPE_CYAN = 3
TYPE_GREEN = 4
...
Con esto, cada nave tendrá almacenado su score. Ahora, tenemos que hacer que cuando una bala del jugador mata a un enemigo, se le sume al puntaje el valor del score de la nave que matamos. Esto se encuentra en la clase CPlayerBullet, cuando chequeamos colisiones contra los enemigos del manager de enemigos.
Pero antes tenemos que solucionar algo. Cuando una bala le pega a un enemigo, debemos incrementar el score del jugador que disparó la bala. Para esto, en la clase CPlayer, al disparar la bala, le pasaremos como parámetro la referencia al jugador que la dispara. Veamos esta línea en la función move(), al disparar:
# Movimiento de la nave.
def move(self):
...
# Disparar.
if fire:
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)
Esto es clave en los juegos y es un concepto muy poderoso. Le pasamos a la bala la referencia self (una referencia a mí mismo). Como estamos en la clase CPlayer, esto es una referencia al jugador que disparó la bala. Entonces, en la clase CPlayerBullet guardamos esa referencia, y con ella podremos acceder a cualquiera de sus funciones. Esto lo usaremos para saber si es el primer jugador o el segundo.
En la clase CPlayer, agregamos una función para preguntar si es el primer jugador o no:
# Retorna True si es el primer jugador. False si es el segundo.
def isPlayerOne(self):
return self.mType == CPlayer.TYPE_PLAYER_1
Y en la clase CPlayerBullet, cuando la bala del jugador le pega a una nave enemiga, se incrementa el score del jugador que corresponda. Veamos los cambios en la clase CPlayerBullet:
# -*- 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 *
# Importar el manager de enemigos para detectar colisiones.
from api.CEnemyManager import *
# Importar la clase para guardar datos.
from game.CGameData import *
# La clase CPlayerBullet hereda de CSprite.
class CPlayerBullet(CSprite):
# Constructor.
# Recibe como parámetro quién disparó la bala.
def __init__(self, aPlayer):
CSprite.__init__(self)
img = pygame.image.load("assets/images/player_bullet.png")
img = img.convert_alpha()
self.setImage(img)
# Guardar la referencia a quién disparó la bala.
self.mPlayer = aPlayer
# Mover el objeto.
def update(self):
CSprite.update(self)
# Detectar choque contra los enemigos.
# Si la bala choca un enemigo, mueren ambos.
enemy = CEnemyManager.inst().collides(self)
if enemy != None:
if enemy.isStateNormal():
print("*****COLISION ENTRE BALA DEL JUGADOR Y ENEMIGO")
enemy.hit()
self.die()
if self.mPlayer.isPlayerOne():
CGameData.inst().addScore1(enemy.getScore())
else:
CGameData.inst().addScore2(enemy.getScore())
# 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)
En el constructor se almacena la referencia al jugador que dispara la bala, y en la función update(), si la bala choca con un enemigo, se usa la referencia al jugador que la disparó, para preguntar si es el primer jugador (si no es el primero, es el segundo) y decidir a quién le asignamos el score.
Nota: Recuerda que como en la lista de enemigos (CEnemyManager) tenemos tanto naves enemigas como balas enemigas, es posible que una bala del jugador choque contra una bala enemiga. Cuando invocamos a la función addScore(), usamos enemy.getScore() para obtener su puntaje, por lo cual puede ser llamado tanto para naves como para balas enemigas (en este caso el score será cero). Debemos asegurarnos que esta función esté en ambos objetos. Por esta razón, hemos puesto el manejo del score en la clase CSprite, así todos los sprites del juego tienen score. Si no hacemos esto, y hubiéramos puesto el score en la clase CNave, cuando la bala choca con una bala enemiga, intenta llamar a esta función y si no existe da error. Hay que tener cuidado con estas cosas. Por eso es muy importante diseñar bien las clases bases del motor (las clases en la carpeta api).
Controlando las Vidas de los Jugadores
Lo que haremos ahora es tener funcional el contador de vidas. Cada vez que se pierda una vida, el contador de vidas debe decrementarse.
Abre el ejemplo que se encuentra en la carpeta capitulo_15\004_vidas_funcional. Si ejecutas el juego, verás que cuando el jugador pierde una vida, se decrementa el contador de vidas y que cuando no le quedan más vidas no vuelve a aparecer la nave. ¿Cómo hacemos esto?
Lo primero, como siempre, es pensar y analizar cuándo es que el contador de vidas debe bajar. Cuando al jugador lo alcanza una bala, parpadea, luego explota y luego vuelve a aparecer (mira la máquina de estados del jugador si no lo recuerdas). Entonces, en el momento en el cual se pasa del estado CPlayer.EXPLODING a CPlayer.START es cuando decrementamos la cantidad de vidas. En este mismo momento, si las vidas ya son cero, no pasamos al estado CPlayer.START, sino que lo pasaremos a un nuevo estado que llamaremos CPlayer.GAME_OVER. Este es un estado nuevo que agregamos, en el cual el jugador no hace nada (en las función render() no se dibuja y en update() no hace nada).
Veamos el código de la función de la clase CPlayer, mostrando en negrita los agregados. Mira en la función update(), en el momento en que termina la explosión y decrementamos el contador de vidas, chequeando si la nave vuelve a comenzar o no:
...
# La clase CPlayer hereda de CAnimatedSprite.
class CPlayer(CAnimatedSprite):
...
# Máquina de estados.
NORMAL = 0
DYING = 1
EXPLODING = 2
START = 3
GAME_OVER = 4
...
# Mover el objeto.
def update(self):
# Invocar update() de CAnimatedSprite.
CAnimatedSprite.update(self)
# Lógica del estado normal.
if self.getState() == CPlayer.NORMAL:
if CEnemyManager.inst().collides(self):
self.setState(CPlayer.DYING)
return
self.move()
# Lógica del estado muriendo.
elif self.getState() == CPlayer.DYING:
if (self.getTimeState() > CPlayer.TIME_DYING):
self.setState(CPlayer.EXPLODING)
return
# Lógica del estado explotando.
elif self.getState() == CPlayer.EXPLODING:
if self.isEnded():
# En este punto comienza una nueva vida.
if self.isPlayerOne():
if (CGameData.inst().getLives1() == 0):
print("LAYER 1 NO JUEGA MAS")
self.setState(CPlayer.GAME_OVER)
else:
CGameData.inst().addLives1(-1)
self.setState(CPlayer.START)
else:
if (CGameData.inst().getLives2() == 0):
print("PLAYER 2 NO JUEGA MAS")
self.setState(CPlayer.GAME_OVER)
else:
CGameData.inst().addLives2(-1)
self.setState(CPlayer.START)
return
# Lógica del estado start.
elif self.getState() == CPlayer.START:
if (self.getTimeState() > CPlayer.TIME_START):
self.setState(CPlayer.NORMAL)
return
self.move()
# En el estado game over no hace nada.
elif self.getState() == CPlayer.GAME_OVER:
return
# 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
...
...
# Establece el estado actual e inicializa
# las variables correspondientes al estado.
def setState(self, aState):
...
elif self.getState() == CPlayer.GAME_OVER:
self.setVisible(False)
...
El código es bastante simple. Es sencillo de seguir. Si es el primer jugador se tiene en cuenta el score del primer jugador. Esto se podría haber hecho con una variable que lleve las vidas en la propia clase CPlayer y pasarle el valor a CGameData. Lo hemos hecho de esta manera por claridad.
En la función update() cuando el estado es CPlayer.GAME_OVER, ya no se hace mas nada (por eso solo ejecuta la sentencia return). Se retorna de la función sin hacer nada.
En la función render() el jugador no se muestra si el jugador está en estado CPlayer.GAME_OVER, y en la función setState() se pone el jugador invisible cuando se pasa a este estado:
Al ejecutar el juego, si perdemos todas las vidas, no podemos seguir jugando y las nave no aparece más. Es hora de trabajar en qué hacemos si ganamos (cuando matamos a todos los enemigos) o perdemos (cuando perdemos todas las vidas).
Condición de Ganar y Condición de Perder
En este punto, estamos listos para agregar al juego el control de la condición de ganar (denominada win condition) y de la condición de perder (denominada lose condition). Esto es, chequear cuando se da la condición para ganar el juego (o el nivel) y cuando se da la condición para perder el juego.
En todos los juegos, la condición para ganar y perder son diferentes, dado que depende del juego que estemos haciendo. En este caso, la condición para ganar el juego es que no queden más enemigos en pantalla y la condición para perder el juego es que ambos jugadores hayan muerto completamente.
Pensando en términos de programación, esto se traduce en:
Ganar: La cantidad de enemigos en la clase CEnemyManager es cero.
Perder: Ambos jugadores están en estado CPlayer.GAME_OVER.
Lo que ocurra primero hará que pasemos a la pantalla de ganar o perder el juego. Por ahora solamente chequeamos la condición de ganar y de perder y mostraremos un mensaje en la consola. Más adelante cambiaremos de pantalla cuando ocurra una de estas condiciones.
Veamos el ejemplo ubicado en la carpeta capitulo_15\005_condicion_ganar_perder. En el programa main.py, en la función update(), chequeamos ambas condiciones:
# Correr la lógica del juego.
def update():
...
# Actualizar los enemigos.
CEnemyManager.inst().update()
# Lógica de los jugadores.
player1.update()
player2.update()
# Detectar la colision entre los dos jugadores.
if player1.collides(player2):
print("COLISION ENTRE LOS JUGADORES")
else:
print("...")
if player1.isGameOver() and player2.isGameOver():
print("AMBOS JUGADORES MUEREN - LOSE CONDITION")
if CEnemyManager.inst().getLength() == 0:
print("TODOS LOS ENEMIGOS MUEREN - WIN CONDITION")
...
Como vemos, el chequeo es simple y estamos usando funciones descriptivas (que tienen nombres que lo hagan fácil de leer). En la clase CPlayer, agregamos una función llamada isGameOver() que nos dice si el jugador está muerto o no. Esta función retorna True si el estado del jugador es CPlayer.GAME_OVER. Esto es, cuando ha terminado de jugar todas sus vidas.
# Retorna True si el jugador está en estado game over (si ya no tiene vidas).
def isGameOver(self):
return self.mState == CPlayer.GAME_OVER
En la clase CManager en la carpeta api (recuerda que es la clase base del managers de enemigos) agregamos una función getLenght(), que simplemente retorna la cantidad de elementos del array. Cuando se trata del manager de enemigos, esto significa la cantidad de enemigos en pantalla.
# Retorna la cantidad de objetos del manager.
def getLength(self):
return len(self.mArray)
De esta forma, básicamente hemos terminado lo que es el juego en sí. Ahora pasaremos en los siguientes capítulos al manejo de las diferentes pantallas que tiene el juego (el menú, los créditos, la pantalla de ayuda, etc), luego al manejo del mouse, y luego de eso, vamos a pulir el juego y a finalizarlo.
Comments