Hasta ahora, hemos aprendido a mover objetos en forma autónoma, pero la inmensa mayoría de los videojuegos para computadoras, necesitan tener un jugador que sea controlado por el jugador, con el teclado en este caso. En este capítulo vamos a agregar una nave en la parte de abajo de la pantalla que se podrá mover con las teclas y más adelante le podrá disparar a las naves enemigas. Haremos una clase CPlayer, que será la nave del jugador, y la misma se moverá a la derecha o a la izquierda con las teclas de dirección (las teclas de flechas). Antes de eso, tenemos que ver cómo se maneja el teclado. Para eso necesitamos ver cómo es el manejo de los eventos.
Manejo de Eventos
Como un videojuego es interactivo, siempre necesitaremos algún control: el teclado, el mouse, un joystick, la pantalla táctil, etc. para controlar algunas cosas, generalmente al jugador. Para hacer esto, debemos capturar los eventos que ocurren en la computadora.
Figura 7-1: Usaremos el teclado para nuestro primer juego.
Un evento es algo que sucede en la computadora (en el Sistema Operativo). Esto puede ser cuando se pulsa una tecla, cuando se presiona el mouse, cuando se cierra la ventana, etc.
Cuando ocurre algo de esto, el sistema operativo genera un evento y se lo envía a la aplicación o actividad (se lo envía a nuestro programa). Entonces, un evento llega a la ventana del juego (que se llama aplicación en Windows o actividad en las computadoras XO, o puede tener otro nombre de acuerdo al sistema operativo).
Si capturamos un evento, podremos reaccionar al mismo (por ejemplo, si se pulsa la flecha derecha, movemos al jugador a la derecha).
El manejo de eventos se hace en el game loop, y ya lo venimos haciendo en los ejemplos anteriores para detectar la tecla [Esc]. Por ejemplo, este código que se encuentra en el game loop es el encargado de capturar el evento de salir y el evento de tecla presionada:
En el game loop de main.py tenemos:
# Correr la lógica del juego.
def update():
...
# Timer que controla el frame rate.
clock.tick(60)
# Procesar los eventos que llegan a la aplicación.
for event in pygame.event.get():
# Si se cierra la ventana se sale del programa.
if event.type == pygame.QUIT:
salir = True
# Si se pulsa la tecla [Esc] se sale del programa.
if event.type == pygame.KEYUP:
if event.key == pygame.K_ESCAPE:
salir = True
...
En cada frame, debemos procesar todos los eventos que se han producido desde el frame anterior y que en este frame han llegado a la ventana. Puede ser que no haya ocurrido ningún evento, o que sean varios eventos los que hayan ocurrido. Pygame pone todos los eventos ocurridos en una lista y nosotros debemos procesar esa lista (a esta tarea se le llama capturar y procesar los eventos). Cabe aclarar que si no procesamos los eventos, el juego no responderá a nada de lo que haga el usuario (por ejemplo, usar el mouse, el teclado, cerrar la ventana, minimizar la ventana, etc.)
Figura 7-2: Varios eventos son enviados a la ventana de la aplicación en el frame actual.
Para procesar los eventos que han ocurrido en el frame actual, usamos una estructura for para recorrer la lista de eventos, que está dada por la función pygame.event.get():
for event in pygame.event.get():
En cada iteración del for, la variable event contiene un objeto con la información del evento que estamos procesando.
Utilizando event.type podemos saber de qué tipo es el evento que ha ocurrido y según el tipo de evento podemos tener más información, como por ejemplo qué tecla se ha pulsado cuando el evento se trata de una pulsación de tecla. Esto lo veremos a continuación.
Chequear la Pulsación de Teclas
Cuando se pulsa una tecla, se recibe un evento en la ventana, y lo capturamos como dijimos antes. Esto ya lo estamos haciendo en el game loop. El código que tenemos ahora para capturar la pulsación de una tecla y luego ver si la tecla pulsada es [Esc], es el siguiente:
En el game loop tenemos:
for event in pygame.event.get():
...
if event.type == pygame.QUIT:
salir = True
if event.type == pygame.KEYUP:
if event.key == pygame.K_ESCAPE:
salir = True
...
Cuando se suelta una tecla, event.type equivale a pygame.KEYUP. Este evento se recibe cuando la tecla es soltada, no cuando es presionada. Una vez que tenemos un evento de teclado, event.key contiene el código de la tecla de la que se trata. Como en este caso queremos salir del programa si la tecla que se pulsó (en el momento en que se suelta) es [Esc], para comparar esa tecla utilizamos su código, que es pygame.K_ESCAPE (esta es una constante, de valor 27, que es el código numérico que representa a esta tecla, y queda más claro usar la constante que usar el valor directamente).
Cada tecla tiene un código asociada a ella (se denomina keycode). Por ejemplo, el código de la tecla [Esc] es 27 (ese número es el valor de la constante pygame.K_ESCAPE).
Afortunadamente, no tenemos porqué acordarnos de los códigos de todas la teclas. Las teclas (como la barra espaciadora, escape o las flechas de dirección, etc.), tienen constantes especiales. Por ejemplo, en lugar de utilizar el valor 27 para la tecla [Esc], lo que hacemos es utilizar la constante pygame.K_ESCAPE (constante definida en Pygame, que tiene el valor 27).
Cada tecla tiene su respectiva constante definida en Pygame. Las teclas que más utilizaremos en los juegos son:
Tecla Constante Descripción
[Arriba] pygame.K_UP Flecha arriba. Usada para subir, saltar o acelerar.
[Abajo] pygame.K_DOWN Flecha abajo. Usada para bajar, agacharse, etc.
[Derecha] pygame.K_RIGHT Flecha derecha. Usada para mover a la derecha.
[Izquierda] pygame.K_LEFT Flecha izquierda. Usada para mover a la izquierda.
[Z] pygame.K_z Generalmente usada para disparar (o saltar).
[X] pygame.K_x Generalmente usada para saltar (o disparar).
[Espacio] pygame.K_SPACE Usada para disparar o saltar.
[Shift Izq.] pygame.K_LSHIFT Shift (mayúsculas) izquierdo. Saltar o disparar.
[Shift Der.] pygame.K_RSHIFT Shift (mayúsculas) derecho. Saltar o disparar.
[Ctrl Izq.] pygame.K_LCTRL Ctrl (control) izquierdo. Saltar o disparar.
[Ctrl Der.] pygame.K_RCTRL Ctrl (control) derecho. Saltar o disparar.
[Alt Izq.] pygame.K_LALT Alt izquierdo. Saltar o disparar.
[Alt Der.] pygame.K_RALT Alt derecho. Saltar o disparar.
[Enter] pygame.K_RETURN Enter. Usada para seleccionar o aceptar.
[Esc] pygame.K_ESCAPE Escape. Usada para cancelar o salir del juego.
En el siguiente enlace se puede ver la lista completa de constantes de teclas definidas en Pygame:
Eventos del Teclado
En un juego necesitaremos saber el momento en el cual se pulsa una tecla y el momento en cuanto se levanta (se suelta) la tecla. Al mover la nave del jugador, la movemos apenas presionamos la tecla de dirección. La nave continúa moviéndose mientras la tecla se encuentre presionada. Cuando se suelte la tecla, la nave dejará de moverse.
Por otra parte, para disparar, debemos reconocer cuando la tecla de disparo es soltada, momento en el que se disparará una bala. Si se dispara la bala cuando la tecla de disparo es presionada, saldría una bala tras otra mientras la tecla de disparo no se suelte (saldría una bala en cada frame). Como no queremos que eso ocurra, dispararemos cuando la tecla se suelte. Más adelante mejoraremos esta forma de disparar.
Ahora veremos cómo capturar estos eventos de teclado. Cuando se pulsa una tecla, event.type será equivalente a pygame.KEYDOWN y cuando se suelta una tecla, event.type será equivalente a pygame.KEYUP. Luego debemos saber qué tecla es la que se presionó o la que se soltó, preguntando por el código de la tecla, que se encuentra en event.key.
Veamos el ejemplo ubicado en la carpeta: capitulo_07\001_eventos_de_teclas.
En este ejemplo, tenemos simplemente un game loop como el primero que hicimos y este programa muestra textos por la consola que dicen cuales teclas apretamos y cuales teclas soltamos. En este programa estamos capturando los eventos de teclado, y leemos las teclas que luego utilizaremos para manejar nuestra nave (usaremos las flechas izquierda y derecha). El siguiente listado muestra la parte del game loop en la cual capturamos los eventos del teclado y mostramos en la consola las teclas que son pulsadas y las teclas que son liberadas.
while not salir:
clock.tick(60)
# Procesar los eventos que llegan a la aplicación.
for event in pygame.event.get():
# Evento cuando se suelta una tecla.
if event.type == pygame.KEYUP:
keyName = pygame.key.name(event.key)
print("Tecla liberada:", keyName)
# Evento cuando se pulsa una tecla.
if event.type == pygame.KEYDOWN:
keyName = pygame.key.name(event.key)
print("Tecla pulsada:", keyName)
if event.key == pygame.K_ESCAPE:
print("[Esc] pulsada")
if event.key == pygame.K_RIGHT:
print("[Derecha] vamos a mover la nave a la derecha")
if event.key == pygame.K_LEFT:
print("[Izquierda] vamos a mover la nave a la izquierda")
...
Con la función pygame.key.name(event.key) obtenemos un string con el nombre de la tecla pulsada. Por ejemplo, si se trata de la tecla [Esc], el texto será "escape". Esto nos puede servir para depurar el programa (buscar errores) si tenemos algún problema con el teclado (sirve para mostrar en la consola las teclas que se presionan). En este programa se muestra el texto de cualquier tecla que se pulsa o se libera.
Si por ejemplo quisieramos saber si se pulsa la tecla [Derecha] o [Izquierda] para mover la nave, comparamos event.key con las constantes pygame.K_RIGHT y pygame.K_LEFT, como se hace en el código anterior. Más adelante en este capítulo agregaremos la nave del jugador y la moveremos con el teclado.
La función pygame.key.get_pressed()
Una característica muy buena que tiene Pygame, es que podemos usar en cualquier momento (y desde cualquier clase) la función pygame.key.get_pressed() para saber qué teclas están siendo pulsadas y cuáles no. Esta función retorna una referencia al array (tupla) donde para cada tecla hay un valor 1 si la tecla está pulsada o 0 si la tecla no está pulsada.
De esta forma, podemos obtener el estado de todas las teclas en cualquier momento. Veamos el ejemplo ubicado en la carpeta: capitulo_07\002_get_pressed.
En este ejemplo simplemente guardamos en una variable la tupla que retorna la función pygame.key.get_pressed(). El código es el siguiente:
# Saber en el momento que teclas están pulsadas.
keys = pygame.key.get_pressed()
Esta tupla contiene un valor booleano (verdadero o falso) para cada tecla. El valor de cada elemento es 1 si la tecla está siendo pulsada y 0 si no lo está. Hay un valor para cada una de las teclas.
Prueba pulsar y soltar teclas para ver como se prenden y apagan los valores booleanos de la tupla.
Para ver si una tecla está siendo pulsada, se accede a la tupla utilizando el nombre de la tecla. Por ejemplo, si queremos saber si el jugador toca la tecla derecha o izquierda se hace:
# Saber en el momento que teclas están pulsadas.
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
print("[Izquierda] vamos a mover la nave a la izquierda")
if keys[pygame.K_RIGHT]:
print("[Derecha] vamos a mover la nave a la derecha")
Utilizando una sentencia print para mostrar la tupla, vemos los valores de cada tecla con claridad.
La pregunta que puede surgir ahora es, si esta forma de chequear las teclas funciona, ¿por qué debemos capturar el evento? La respuesta es que tenemos muchas cosas en Pygame que se hacen por nosotros, y que las podemos utilizar si nos hacen la programación más sencilla, pero debemos comprender cómo funciona todo el sistema.
Por ejemplo, si mañana quisiéramos desarrollar en otro lenguaje como Java o C++, no tendremos una función como pygame.key.get_pressed() que nos facilite la programación, pero sabemos que las ventanas funcionan con un sistema de eventos y que en el game loop tenemos que capturar los eventos del teclado que llegan a la ventana. Luego, todo lo que venga dado en el lenguaje o motor que nos facilite la escritura mejor, pero es importante conocer completamente el funcionamiento del sistema para aprender correctamente los fundamentos. Por esta razón, esta no será la primera vez que se explique algo y se programe, solo para ver más adelante que Pygame ya lo tiene implementado para nosotros.
Por otra parte, más adelante en este capítulo vamos a hacer una clase CKeyboard donde pondremos funciones que no tiene Pygame, como por ejemplo una función firstPress() que nos dirá si una tecla ha sido pulsada por primera vez, cosa que más adelante veremos que es muy útil en los juegos para saltar o disparar.
Ahora que podemos leer las teclas, vamos a agregar la nave del jugador al juego y a moverla con las teclas izquierda y derecha.
Agregando el Jugador
Vamos a agregar la nave del jugador al juego, colocándola en la parte inferior de la pantalla. El jugador controlará la nave moviéndose a la izquierda y a la derecha con las teclas [Izquierda] y [Derecha] respectivamente. Más adelante también podremos disparar.
Veamos el ejemplo que se encuentra en la carpeta: capitulo_07\003_agregar_el_jugador.
La nave se encuentra en el archivo player00.png. La imagen tiene tiene un tamaño de 56 x 42 píxeles. Como siempre, colocamos la imagen en la carpeta assets/image. En la siguiente figura se puede ver cómo luce la misma.
Figura 7-3: La nave que controla el jugador.
Lo que hacemos es escribir una clase CPlayer, que se encargará de manejar la lógica del jugador. Al igual que como hicimos con los enemigos, vamos a tener un tipo de jugador (para luego poder tener dos jugadores). Por ahora tendremos un solo jugador.
La clase CPlayer es muy parecida a la clase CNave. Por ahora no nos preocupamos de este problema de diseño (o sea, el tener clases muy parecidas). No es conveniente tener clases duplicadas o muy parecidas para diferentes tipos de objetos (la nave y el jugador en este caso) y que tengan el mismo código. La solución va a ser tener una clase CSprite de la cual heredarán todos los objetos del juego. Más adelante haremos esto.
La clase CPlayer va en el package game (porque es una clase propia del juego), y queda de la siguiente manera:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Clase CPlayer.
# Nave que controla el jugador.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CGameObject.
from api.CGameObject import *
# La clase CPlayer hereda de CGameObject.
class CPlayer(CGameObject):
# Tipos de jugador.
TYPE_PLAYER_1 = 0
TYPE_PLAYER_2 = 1
# ---------------------------------------------------------------
# Constructor. Recibe el tipo de nave (TYPE_PLAYER_1 o TYPE_PLAYER_2).
# ---------------------------------------------------------------
def __init__(self, aType):
# Invocar al constructor de la clase base.
CGameObject.__init__(self)
# Segun el tipo de la nave, la imagen que se carga.
self.mType = aType
if self.mType == CPlayer.TYPE_PLAYER_1:
imgFile = "assets/images/player00.png"
# Cargar la imagen.
self.mImg = pygame.image.load(imgFile)
self.mImg = self.mImg.convert_alpha()
# Guardar el ancho y el alto.
self.mWidth = self.mImg.get_width()
self.mHeight = self.mImg.get_height()
# Mover el objeto.
def update(self):
# Invocar a update() de la clase base para el movimiento.
CGameObject.update(self)
# Dibuja el objeto en la pantalla.
# Parámetros:
# aScreen: La superficie de la pantalla en donde dibujar.
def render(self, aScreen):
aScreen.blit(self.mImg, (self.mX, self.mY))
# Liberar lo que haya creado el objeto.
def destroy(self):
# Invocar a destroy() de la clase base.
CGameObject.destroy(self)
self.mImg = None
# Retorna el ancho de la nave.
def getWidth(self):
return self.mWidth
# Retorna el alto de la nave.
def getHeight(self):
return self.mHeight
En el programa principal, main.py, importamos la clase game.CPlayer agregamos una variable player para el jugador, creamos el jugador y luego invocamos como siempre a sus funciones update() y render() en el game loop y destroy() al salir del juego. Se muestra a continuación el código, resaltado en negrita las líneas agregadas.
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Agregando la nave controlada por el jugador.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CNave.
from game.CNave import *
# Importar la clase Cuadrado.
from game.CCuadrado import *
# Importar la clase CPlayer.
from game.CPlayer import *
# Definir ancho y alto de la pantalla.
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 360
RESOLUTION = (SCREEN_WIDTH, SCREEN_HEIGHT)
# Control del modo ventana o fullscreen.
isFullscreen = False
screen = None
imgBackground = None
imgSpace = None
clock = None
n1 = None
n2 = None
n3 = None
c1 = None
c2 = None
c3 = None
player = None
# Inicializar la variable de control del game loop.
salir = False
# Función de inicialización.
def init():
global screen
global imgBackground
global imgSpace
global n1
global n2
global n3
global c1
global c2
global c3
global clock
global player
# Inicializar Pygame.
pygame.init()
# Poner el modo de video en ventana e indicar la resolución.
screen = pygame.display.set_mode(RESOLUTION)
# Poner el título de la ventana.
pygame.display.set_caption("Mi Juego")
# Crear la superficie del fondo o background.
imgBackground = pygame.Surface(screen.get_size())
imgBackground = imgBackground.convert()
# Cargar la imagen del fondo. La imagen es de 640 x 360.
imgSpace = pygame.image.load("assets/images/space_640x360.jpg")
imgSpace = imgSpace.convert()
# Dibujar la imagen cargada en la imagen de background.
imgBackground.blit(imgSpace, (0, 0))
# Crear las naves: se le pasa como parámetro la imagen de la nave.
n1 = CNave(CNave.TYPE_PLATINUM)
n2 = CNave(CNave.TYPE_GOLD)
n3 = CNave(CNave.TYPE_RED)
# Colocar las naves en su posición inicial.
n1.setXY(0, 100)
n2.setXY(0, 150)
n3.setXY(0, 200)
# Las naves comienzan detenidas.
n1.setVelX(0)
n1.setVelY(0)
n2.setVelX(0)
n2.setVelY(0)
n3.setVelX(0)
n3.setVelY(0)
# Acelerar las naves.
n1.setAccelX(0.1)
n1.setAccelY(0)
n2.setAccelX(0.2)
n2.setAccelY(0)
n3.setAccelX(0.3)
n3.setAccelY(0)
# Marcar los límites del mundo.
n1.setBounds(-n1.getWidth(), -n1.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n2.setBounds(-n2.getWidth(), -n2.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n3.setBounds(-n3.getWidth(), -n3.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
c1 = CCuadrado(20, 20, (255,0,0))
c2 = CCuadrado(20, 20, (0, 255, 0))
c3 = CCuadrado(20, 20, (0, 0, 255))
c1.setXY(0, 50)
c2.setXY(0, 80)
c3.setXY(0, 110)
c1.setVelX(0.5)
c1.setVelY(0)
c2.setVelX(1.0)
c2.setVelY(0)
c3.setVelX(1.5)
c3.setVelY(0)
# Crear el jugador.
player = CPlayer(CPlayer.TYPE_PLAYER_1)
player.setXY(SCREEN_WIDTH / 2 - player.getWidth() / 2, SCREEN_HEIGHT - player.getHeight())
# Establecer los límites de la pantalla.
player.setBounds(0, 0, SCREEN_WIDTH - player.getWidth(), SCREEN_HEIGHT)
# Inicializar el reloj.
clock = pygame.time.Clock()
# Correr la lógica del juego.
def update():
global salir
global screen
global isFullscreen
# Timer que controla el frame rate.
clock.tick(60)
# Procesar los eventos que llegan a la aplicación.
for event in pygame.event.get():
# Si se cierra la ventana se sale del programa.
if event.type == pygame.QUIT:
salir = True
# Si se pulsa la tecla [Esc] se sale del programa.
if event.type == pygame.KEYUP:
if event.key == pygame.K_ESCAPE:
salir = True
# Si se pulsa la tecla [F], se cambia entre ventana y fullscreen.
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_f:
isFullscreen = not isFullscreen
if isFullscreen:
screen = pygame.display.set_mode(RESOLUTION, pygame.FULLSCREEN)
else:
screen = pygame.display.set_mode(RESOLUTION)
# Movimiento de la nave con las teclas.
if event.type == pygame.KEYDOWN:
if (event.key == pygame.K_RIGHT):
player.setVelX(4)
if (event.key == pygame.K_LEFT):
player.setVelX(-4)
if event.type == pygame.KEYUP:
if (event.key == pygame.K_RIGHT):
player.setVelX(0)
if (event.key == pygame.K_LEFT):
player.setVelX(0)
# Lógica de las naves.
n1.update()
n2.update()
n3.update()
c1.update()
c2.update()
c3.update()
# Lógica del jugador.
player.update()
# Dibujar el frame y actualizar la pantalla.
def render():
# Dibujar el fondo.
screen.blit(imgBackground, (0, 0))
# Dibujar las naves en la nueva posición.
n1.render(screen)
n2.render(screen)
n3.render(screen)
c1.render(screen)
c2.render(screen)
c3.render(screen)
# Dibujar el jugador.
player.render(screen)
# Actualizar la pantalla.
pygame.display.flip()
# Función de destrucción.
def destroy():
global n1
global n2
global n3
global c1
global c2
global c3
global player
# Destruir los objetos.
n1.destroy()
n1 = None
n2.destroy()
n2 = None
n3.destroy()
n3 = None
c1.destroy()
c1 = None
c2.destroy()
c2 = None
c3.destroy()
c3 = None
# Destruir la nave del jugador.
player.destroy()
player = None
# Cerrar Pygame y liberar los recursos que pidió el programa.
pygame.quit()
# ============= Punto de entrada del programa. =============
# Inicializar los elementos necesarios del juego.
init()
# Loop principal del juego.
while not salir:
# Actualizar los objetos.
update()
# Dibujar la pantalla.
render()
# Liberar los recursos al final.
destroy()
Como vemos en el código anterior, en las partes resaltadas en negrita se muestra el código que crea y controla a la nave del jugador. Se importa la clase al inicio, se crea una variable player, se crea el objeto y colocamos la nave en el medio de la pantalla y en la parte inferior.
Figura 7-4: Nuestro juego ahora tiene la nave que controla el jugador.
El siguiente código es el que crea el jugador:
# Crear el jugador.
player = CPlayer(CPlayer.TYPE_PLAYER_1)
player.setXY(SCREEN_WIDTH/2 - player.getWidth()/2, SCREEN_HEIGHT -
player.getHeight())
Al colocar el jugador en su posición inicial, se toma en cuenta su ancho y alto para que la nave aparezca exactamente en el medio de la pantalla y en el piso, sobre la parte inferior de la pantalla (por eso se resta el alto de la nave al alto de la pantalla). Recuerda que el punto de registro (donde se posiciona la nave) es la esquina superior izquierda del dibujo.
Para mover al jugador, detectamos si se pulsa una tecla y si eso ocurre se le pone una velocidad al jugador. Cuando se suelta la tecla se pone la velocidad en cero, lo que hace que se detenga. A continuación se muestra el código que mueve la nave con el teclado:
def update():
...
# Procesar los eventos que llegan a la aplicación.
for event in pygame.event.get():
...
# Movimiento de la nave con las teclas.
if event.type == pygame.KEYDOWN:
if (event.key == pygame.K_RIGHT):
player.setVelX(4)
if (event.key == pygame.K_LEFT):
player.setVelX(-4)
if event.type == pygame.KEYUP:
if (event.key == pygame.K_RIGHT):
player.setVelX(0)
if (event.key == pygame.K_LEFT):
player.setVelX(0)
...
# Lógica del jugador.
player.update()
No podemos olvidarnos de invocar a la función update() de la nave, para que se actualice su movimiento, ni olvidarnos de invocar a render() para dibujarla. En los primeros objetos que agreguemos al juego, es muy posible que olvidemos algo, y entonces la nave no se moverá o no se verá en pantalla. Cuando esto ocurra, vuelve al código de los ejemplos y comprueba línea a línea las diferencias.
Cuando ejecutamos el programa, notamos que el juego tiene varios problemas. El primer problema es que si pulsamos dos teclas a la vez, a veces la nave se tranca.
El segundo problema es que el código que controla al jugador se encuentra en el programa main.py, cuando debería estar en la clase CPlayer (es más natural que si luego queremos corregir cómo se mueve el jugador, vayamos a la clase CPlayer, no al código de main.py).
Estos dos problemas los solucionaremos cuando hagamos una clase para la lectura del teclado. Ahora no podemos solucionarlo con lo que tenemos, por lo cual lo dejaremos así por el momento.
Existe un tercer problema, que es que si la nave se va de de la pantalla, aparecerá del otro lado de la misma. Esto es así porque el código que controla los bordes se encuentra en la clase CGameObject, y es el mismo comportamiento para todos los objetos del juego. Debemos poder indicar qué comportamiento queremos cuando se alcanza un borde, dado que el juego tendrá diferentes objetos que se comportan en diferentes formas. Esto lo vamos a corregir más adelante cuando agreguemos varios comportamientos al tocar los bordes y podamos elegir cuál usamos (algunos objetos al tocar el borde desaparecen, como las balas, otros se detendrán, como la nave del jugador, otros aparecerán por el lado contrario, etc.).
El último problema que existe es que en este momento las clases CNave y CPlayer son muy similares entre sí. Debemos hacer una clase CSprite que se encargue del manejo de las imágenes y de las animaciones. Esto también lo haremos más adelante.
A continuación arreglaremos el primer y segundo problema, que se solucionan haciendo una clase CKeyboard para manejar el teclado. Más adelante solucionaremos los problemas restantes cuando veamos el manejo de Sprites.
La Clase CKeyboard
Escribiremos una clase CKeyboard que contendrá en todo momento la información de qué teclas son pulsadas o soltadas. Esto es de suma utilidad para controlar objetos con el teclado fácilmente.
Cuando un evento de teclado llega a la ventana, el evento será capturado y será pasado a la clase CKeyboard, quien se encargará de guardar el estado de las teclas que necesitemos para el juego. Esta técnica se denomina hardware polling (hacer polling significa preguntar siempre por el estado de algo, como por ejemplo las teclas en este caso). En realidad, para mover un personaje en los juegos de acción, no nos interesa saber cuando se suelta o se pulsa una tecla, sino saber si la tecla está pulsada o no en un momento dado (en el frame actual).
De esta forma, podremos saber en cada momento si una tecla está pulsada o no, lo cual permitirá que cualquier objeto del juego pueda chequear teclas. Con la clase CKeyboard funcionando, el código que se encarga de manejar la nave del jugador no estará en main.py, sino en la clase CPlayer, como es natural pensar que ahí es donde debe estar. El código le pondrá velocidad al jugador cuando la tecla está pulsada y se detendrá cuando la tecla no esté pulsada. Para esto se consultarán las variables que tendremos en la clase CKeyboard.
Veamos el ejemplo que se encuentra en la carpeta: capitulo_07\004_clase_keyboard.
Si corremos el ejemplo, veremos que la nave se mueve bien y no se tranca. Abre el programa main.py.
Lo primero que se hace es importar la clase:
# Importar la clase CKeyboard
from api.CKeyboard import *
Luego, en la función update(), se captura el evento de cuando se pulsa una tecla y de cuando se suelta y se llama a la función keyDown() y keyUp() respectivamente, para informarle a la clase CKeyboard que se pulsaron o soltaron teclas:
def update():
...
for event in pygame.event.get():
...
# Registrar cuando se apreta o suelta una tecla.
if event.type == pygame.KEYDOWN:
CKeyboard.inst().keyDown(event.key)
if event.type == pygame.KEYUP:
CKeyboard.inst().keyUp(event.key)
La clase CKeyboard se encuentra en el package api, dado que es una clase que se reutilizará entre distintos juegos. Esta clase contiene estas dos funciones, keyDown() y keyUp(), que se llaman desde el game loop cuando se pulsa una tecla y cuando se libera. Como vemos en este código, para usar la clase CKeyboard no es necesario crear ningún objeto (es una clase singleton). Más adelante hablaremos sobre esto. El código de la clase CKeyboard es el siguiente:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Clase CKeyboard.
# Clase para manejar el estado de las teclas en el juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
class CKeyboard(object):
mInstance = None
mInitialized = False
mLeftPressed = False
mRightPressed = False
def __new__(self, *args, **kargs):
if (CKeyboard.mInstance is None):
CKeyboard.mInstance = object.__new__(self, *args, **kargs)
self.init(CKeyboard.mInstance)
else:
print("Cuidado: CKeyboard(): No se debería instanciar más de una vez esta clase. Usar CKeyboard.inst().")
return CKeyboard.mInstance
@classmethod
def inst(cls):
if (not cls.mInstance):
return cls()
return cls.mInstance
def init(self):
if (CKeyboard.mInitialized):
return
CKeyboard.mInitialized = True
CKeyboard.mLeftPressed = False
CKeyboard.mRightPressed = False
def keyDown(self, key):
if (key == pygame.K_LEFT):
CKeyboard.mLeftPressed = True
if (key == pygame.K_RIGHT):
CKeyboard.mRightPressed = True
def keyUp(self, key):
if (key == pygame.K_LEFT):
CKeyboard.mLeftPressed = False
if (key == pygame.K_RIGHT):
CKeyboard.mRightPressed = False
def leftPressed(self):
return CKeyboard.mLeftPressed
def rightPressed(self):
return CKeyboard.mRightPressed
def destroy(self):
CKeyboard.mInstance = None
Esta clase lo que hace es almacenar dos variables con el estado de la tecla derecha e izquierda (hay una variable para cada tecla). Las variables mRightPressed y mLeftPressed contienen el estado de las teclas derecha e izquierda respectivamente. Estas variables contienen True si la tecla está siendo pulsada y False si no lo está.
Luego tenemos dos funciones para preguntar por el estado de estas teclas. La función rightPressed() retornará True si la tecla derecha está apretada y False si no lo está. De forma similar, la función leftPressed() retorna el estado de la tecla izquierda.
Ignora por ahora el comienzo de la clase CKeyboard, ya explicaremos qué hace esa parte.
Para mover al jugador utilizando el teclado, en la clase CPlayer, en la función update(), se mueve el jugador según las teclas que se hayan pulsado. En la clase CPlayer tendremos:
...
# Importar la clase CKeyboard
from api.CKeyboard import *
...
def update(self):
# Mover el jugador con el teclado.
if not CKeyboard.inst().leftPressed() and not CKeyboard.inst(). rightPressed():
self.setVelX(0)
else:
if CKeyboard.inst().leftPressed():
self.setVelX(-4)
elif CKeyboard.inst().rightPressed():
self.setVelX(4)
# Invocar a update() de la clase base para el movimiento.
CGameObject.update(self)
...
Como podemos ver, en cualquier objeto y en cualquier momento podemos consultar si las teclas [Derecha] o [Izquierda] están pulsadas. Esto es posible simplemente utilizando CKeyboard.inst().leftPressed() que retorna True si la tecla izquierda está pulsada y False si no lo está.
Si corremos el ejemplo, podemos ver que la nave ya no se tranca. Esto es porque ahora tenemos la información correcta en la clase CKeyboard. Si la tecla derecha está siendo pulsada, se pone la velocidad positiva y la nave se mueve a la derecha. Si se está pulsando la tecla izquierda, se pone la velocidad negativa y la nave se mueve hacia la izquierda. Si ninguna de las dos teclas se pulsa, la velocidad se pone en cero, por lo cual la nave se queda quieta. No hay margen para el error en la lógica escrita de esta manera y es clave escribir los juegos sin errores.
Ahora bien, ¿por qué podemos usar la clase CKeyboard directamente de esta forma? (sin crear un objeto de la clase CKeyboard). La respuesta es que se ha utilizado una clase que es Singleton. Esto es lo que se explica a continuación.
Una Clase Singleton
En muchas ocasiones es preferible tener una sola instancia de una clase. Este es el caso de la clase CKeyboard, porque queremos que en el programa exista sólo una instancia de esta clase. No necesitamos dos o más clases para manejar el teclado, necesitamos una sola.
Por esta razón es que esta clase es un singleton. Un singleton es un patrón de diseño que se utiliza cuando es necesario tener una única instancia de una determinada clase. En este caso, queremos que exista un único objeto de la clase CKeyboard, o sea, una sola instancia de esta clase.
Para eso, tenemos un atributo de la clase, mInstance, el cual contendrá la referencia al único objeto creado de la clase (en este caso, el único objeto CKeyboard que existirá). Cuando accedemos a la clase y se intenta crear un objeto CKeyboard, éste se crea sólo la primera vez (cuando mInstance es None) en el constructor (ver la función __new__). En el programa, simplemente usamos CKeyboard.inst() para acceder a la clase CKeyboard. Si la instancia no existe, se crea, y si ya existiera, simplemente se retorna. De esta forma la clase es un singleton, y se asegura que solamente haya una instancia.
A continuación se puede ver el código de CKeyboard que implementa el patrón Singleton.
...
class CKeyboard(object):
mInstance = None
mInitialized = False
...
def __new__(self, *args, **kargs):
if (CKeyboard.mInstance is None):
CKeyboard.mInstance = object.__new__(self, *args, **kargs)
self.init(CKeyboard.mInstance)
else:
print(“Cuidado: CKeyboard(): No se debería instanciar más de una vez esta clase. Usar CKeyboard.inst().”)
return CKeyboard.mInstance
@classmethod
def inst(cls):
if (not cls.mInstance):
return cls()
return cls.mInstance
def init(self):
if (CKeyboard.mInitialized):
return
CKeyboard.mInitialized = True
...
El patrón Singleton lo utilizaremos para todas las clases del juego que deban ser únicas, por ejemplo, la clase CMouse que se encarga del manejo del mouse (porque hay un solo mouse), la clase CBulletManager que se encargará de los disparos del juego (porque hay un solo manejador de balas), en general todos los managers que sean únicos, etc.
Al tener la clase CKeyboard definida como singleton, un sprite (u otra clase cualquiera del juego) en cualquier momento puede preguntar por el estado de las teclas. En general, haremos singleton a todas las clases del juego que deban existir una vez sola y para que cualquier clase pueda acceder a ellas (este patrón de diseño brinda un acceso global a la clase desde cualquier otra clase). Ejemplos de clases singleton son: CKeyboard, CMouse, los managers (listas) de enemigos o balas, el nivel, el sistema de partículas, etc.
Mejorando la Clase CKeyboard
Por ahora en la clase CKeyboard tenemos funciones para chequear el estado de las teclas de dirección para la izquierda y para la derecha, pero nos faltan muchas otras teclas que vamos a necesitar. Por ahora esto lo dejaremos así, y conforme vayamos haciendo el juego le iremos agregando funcionalidad a esta clase.
A veces es necesario programar varios juegos para saber todo lo que puede necesitar una clase de base como la clase CKeyboard. Por ahora dejaremos la clase así, y cuando agreguemos otro jugador, o disparemos, necesitaremos modificar la clase CKeyboard. Ya veremos esto cuando lleguemos a ese punto.
En el próximo capítulo veremos el manejo de sprites.
Comments