BIOMETANO SÍ, PERO NO ASÍ. LA NUEVA BURBUJA ENERGÉTICA
Programación con Pygame VII
1. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
Aventura: Ficción Interactiva
(Parte del código de recorte basado en un script original de Tiny Ducks On Wheels)
Introducción
El objetivo del programa es aprender una serie de técnicas y disponer de código que nos
sirva para realizar juegos de ficción interactiva con PyGame, es decir, juegos
conversacionales (del estilo de “The House”) con imágenes. Para ello, el programa se
organiza en torno a tres clases y una función que podéis modificar o ampliar en vuestros
propios programas:
1. La clase Narrar
Un objeto de este tipo es el que se utilizar para poner en pantalla la descripción del
lugar en el que se encuentra el jugador. También puede usarse para responder a sus
peticiones. Cuando se crea el objeto, hay que proporcionarle una surface (sobre la
que se dibujará), un rect (que marcará la zona donde ha de dibujarse), un tipo de letra
y un color (que se emplearán al dibujar el texto) y el color de fondo.
2. La clase Hablar
El objeto de este tipo hace las veces del cuadro donde el jugador escribe las acciones
que desea realizar. Al crearse el objeto, debemos pasarle también una surface y un
rect, un tipo de letra, su color y el color de fondo.
PÁGINA 1 DE 10
CC: FERNANDO SALAMERO
2. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
3. La clase Juego
Se trata del tipo de objeto más importante del juego, pues representa el status en el
que se encuentra; localización, acciones... Como allí donde esté el jugador debe
proporcionársele una descripción del entorno y, además, debe poder decir lo que
quiere hacer, esta clase está ligada a las clases Narrar y Hablar. En consecuencia,
cuando se crea uno de estos objetos hay que proporcionarle, una vez más, una surface
(sobre lo que dibujar), una imagen (que representará el lugar en el que nos
encontramos), un objeto de tipo Narrar y otro de tipo Hablar.
4. La función procesar()
Esta función se encargará de procesar las órdenes del jugador para realizar las
correspondientes acciones, es decir, es el parser. En un juego completo, aquí está el
núcleo del desarrollo de la aventura, ya que se usará para tomar objetos, cambiar de
localización, luchar, etc. Como las acciones dependerán de la situación en la que nos
encontremos en el juego, cuando se invoca a la función se le ha de pasar como
argumento un objeto de tipo Juego.
Como quiera que el programa tiene por objetivo aprender la técnica, no es un juego
realmente operativo. Sólo están implementados los comandos “norte”, “sur” y “salir” y sólo
están incluidos dos lugares (y por tanto, dos imágenes).
Veamos el código:
PÁGINA 2 DE 10
CC: FERNANDO SALAMERO
3. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
Importación de Librerías y Definición de Constantes
Ya de sobras conocido; las librerías habituales, la inicialización de PyGame, la creación del
objeto visor con la surface del juego y las declaraciones de los colores que vamos a usar
con los textos y el diálogo.
Clase JUEGO
1. En el constructor de la clase, es decir, la función __init__(), lo que hacemos es
almacenar como atributos los argumentos que se le pasan para que la clase los pueda
utilizar posteriormente cuando se desee. Estos atributos (a los que se accede como
sabemos, con self.nombre_del_atributo) son surface, image (hemos usado image y
no imagen, como nombre, por similitud a la clase pygame.sprite.Sprite), narrar y
hablar. Ya hemos explicado más arriba su significado.
2. La función update() se encarga de actualizar y dibujar en pantalla la situación en la
que nos encontramos en el juego (observa, de nuevo, que hemos elegido un nombre
familiar...). Fíjate el orden en el que lo hacemos. Primero se dibuja la imagen de fondo
(‘el paisaje’). A continuación se llama al método update() del objeto narrar para
que dibuje sobre el paisaje lo que ocurre en el juego. Finalmente se llama a la función
escuchar() del propio objeto para que, como veremos enseguida, se esté atento al
teclado y se procesen las teclas que se vayan pulsando.
3. escuchar() se encarga de ir escribiendo en pantalla lo que se escribe hasta que se
pulsa la tecla return (K_RETURN). La técnica es interesante, pues al contrario que
con los programas escritos para la linea de comandos, en las surfaces de PyGame no
se puede escribir directamente al estilo del conocido raw_input(). Veamos cómo se
ha implementado:
Lo primero es calcular cuántas letras nos caben en el cuadrado de texto que hará las
veces de diálogo. Veremos en breve que el objeto hablar tiene un atributo de tipo
Rect (que llamaremos rect) que indica la zona en la que se va a poner en pantalla. Así
que podemos averiguar su anchura total en pixeles con hablar.rect.w . Si lo
dividimos por la anchura de una letra típica, como la letra ‘a’, sabremos cuántas letras
nos van a caber. Afortunadamente, el objeto de tipo hablar tendrá también un
atributo de tipo Font (llamado tipoLetra) que posee (mira la documentación) una
función miembro ( size() ) que nos da la anchura en pixeles de un carácter.
¡Estupendo! Almacenaremos cuántas letras nos caben en la variable maximo. Es más
fácil verlo que contarlo:
maximo = self.hablar.rect.w / self.hablar.tipoLetra.size('a')[0]
(el [0] del final es por que, como habrás visto en la documentación, la función size()
nos devuelve una tupla con la anchura y la altura, así que nos interesa el primer
elemento).
Probablemente, si has probado a escribir texto con PyGame habrás tenido problemas
con los acentos o con los caracteres no anglosajones como ‘ñ’. Para que se muestren
correctamente, necesitamos pasarle los textos con codificación unicode. No te
preocupes; basta añadirle una u como prefijo a una cadena de texto para que se
considere como unicode. Es por eso por lo que se ha escrito
self.hablar.frase = u''
PÁGINA 3 DE 10
CC: FERNANDO SALAMERO
4. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
para inicializar una cadena de texto (en blanco) a la que se añadirán las teclas que se
vayan pulsando. De nuevo, frase será un atributo del objeto hablar que se encargará
de memorizar las órdenes que se vayan dando en el juego.
Lo siguiente es dibujar el cuadrado que enmarca la zona donde se escribirán nuestras
ordenes. La función cuadrado() del objeto hablar se encargará de ello. Naturalmente,
si queremos que se muestre inmediatamente en pantalla, tendremos que invocar a
pygame.display.update().
Vamos a la parte final. Necesitamos un bucle que vaya capturando las teclas pulsadas y
que termine cuando se pulse return. La variable booleana hablando nos ayudará en
la tarea; dentro del bucle, miramos la cola de eventos de la forma habitual y si la
tecla pulsada es K_RETURN, hablando se pone a False, el bucle acabará y se
saldrá de la función escuchar().
Si la tecla pulsada es otra, querremos escribirla en pantalla. Afortunadamente,
PyGame dispone de evento.unicode que nos da, precisamente, el texto de la tecla
pulsada en codificación unicode. La función que se encargará de usar esta pulsación
para escribir en pantalla será la función update() del objeto hablar:
self.hablar.update(evento.unicode)
Un matiz. Si la tecla pulsada es la de borrar (K_BACKSPACE), querremos que se
elimine la última tecla pulsada, de allí la instrucción
self.hablar.frase = self.hablar.frase[:-1]
que hace precisamente eso (y no hay que pasarle ningún valor para dibujar). Además,
hay que controlar que el número de teclas pulsadas no superen el máximo deseado (en
caso contrario, el texto se saldría del cuadro), lo que se consigue mirando si
len(self.hablar.frase) < maximo.
Clase Hablar
Se encarga de mostrar y almacenar las órdenes del jugador.
1. __init__(), como es habitual, guarda como atributos los valores que se pasan al
objeto cuando se crea. Además, se definen dos atributos más: margen marca el
tamaño del margen, ya que si el texto comienza a escribirse justo en el borde del
cuadro queda poco elegante; y frase contendrá, como hemos visto, lo que escribe el
jugador.
2. La función cuadrado() realza la zona en la que se va a escribir. Para que no tape la
parte de la imagen de fondo que tiene detrás tendremos que aplicarle transparencia.
¿Cómo hacerlo? Primero creamos una surface del tamaño del rect del objeto que
llamamos cuadrado. Indicamos que vamos a usar transparencia con el ya conocido
convert_alpha(). ¿Cuánta transparencia? Eso se indica con set_alpha() ; cuanto
menor sea el valor que le pasemos, tanto más transparente será. Finalmente, pintamos
el cuadrado entero del color deseado con fill() y se dibuja el resultado sobre la
surface que se ha pasado al objeto (típicamente, nuestro visor).
3. Al método update(), si recordamos lo que hemos visto en la clase Juego, le pasamos
el valor de evento.unicode (la tecla pulsada). Ese parámetro lo hemos llamado tecla
en la implementación de la función. El objetivo es añadir dicha tecla a la variable frase
y dibujar el resultado.
PÁGINA 4 DE 10
CC: FERNANDO SALAMERO
5. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
Las primeras dos líneas posicionan dónde va a escribirse, dejando el margen
pertinente. A continuación se añade a frase el valor de tecla (todo está en unicode,
así que es correcto), se dibuja el marco semitransparente, se dibuja el texto y se
actualiza la pantalla para que se muestre. ¡Muy bien!
Clase Narrar
Muestra la descripción del lugar donde se encuentra el jugador o cualquier otro mensaje
que se desee como respuesta a una acción, a través de su atributo texto. La idea es hacerlo
de forma similar a la clase anterior pero ahora el problema es distinto. El texto entero ya lo
tenemos pero, por contra, puede ser muy largo y ocupar muchas líneas. La clase se
encargará de ajustar automáticamente el texto al tamaño disponible.
1. __init__() almacena, como es habitual, los parámetros con los que se crea el objeto
en atributos propios (para usarlos posteriormente). Hay un añadido, como ya hemos
comentado, texto.
2. cortaTexto() es un método que toma como argumento un número que representa el
tamaño máximo en pixeles que puede ocupar una línea de texto. El objetivo de la
función es, partiendo del texto almacenado en texto, dividirlo en una lista de líneas
que respeten el ancho dado. Con ello se consigue que sea cual sea el valor de texto, se
ajuste y quepa dentro de la zona donde se va a escribir.
Para realizarlo, y usando nuestra vieja amiga split(), el texto se divide en una lista de
líneas de partida (para conservar los puntos y aparte que pudiera tener):
lineas = self.texto.split("n")
(el código especial ‘n’ es la marca de nueva línea en Linux). A continuación creamos
una lista vacía, resultado, en donde se irán añadiendo las lineas con el tamaño
correcto.
El resto del trabajo se realiza con un bucle for que recorre todas las lineas anteriores.
Para cada una de ellas, palabras contiene su lista de palabras (de nuevo usando
split()). La idea es ir añadiendo palabras, calcular cuanto tamaño tiene cada una, y si
no se supera el tamaño máximo ir a por la siguiente. En el momento en el que se
supere dicho tope, se cambia de linea (es decir, se añade la línea a la lista y se
empiezan a mirar las siguientes palabras). Intenta seguir el proceso. Ten en cuenta que
se calcula el ancho en pixeles de un texto con size(), que se añaden elementos a la lista
con el método append() de los objetos list, y que para concatenar textos se usa el
método join() de los objetos str. En cualquier caso, la función termina devolviendo
como valor de salida a resultado, la lista de líneas deseada.
3. La función update() toma como único argumento un número, margen, cuyo
significado es evidente por el nombre. Comenzamos calculando la posición en la que
situar el texto horizontalmente, dejando el margen y obteniendo la lista de líneas que
se van a escribir, lineas, llamando al método cortaTexto() que hemos visto hace un
momento. Observa que se ha tenido en cuenta también el margen para indicar el
máximo tamaño disponible (se resta dos veces, contando ambos lados).
Para centrar el texto verticalmente, necesitamos saber cuanto ocupan las líneas. En
nuestro auxilio viene el método get_linesize() de los objetos Font. Nos devuelve la
altura en pixeles que ocupa una linea de texto con ese tipo de letra. Una vez conocido,
para conseguir que el texto quede en la parte central de la zona de impresión,
calculamos su coordenada vertical así:
PÁGINA 5 DE 10
CC: FERNANDO SALAMERO
6. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
posy = self.rect.top + (self.rect.h - alturaLinea * len(lineas)) / 2
(observa que a la altura de la zona que ocupa el texto, se le resta la altura total de todas
las líneas y el resultado se divide por dos para que el margen sea el mismo por arriba
que por abajo; probablemente necesitarás hacerte un dibujo para entenderlo).
Ya podemos pasar a dibujar. Primero, de forma similar a como hacía la función
cuadrado() en la clase Hablar, dibujamos el cuadrado de fondo semitransparente.
A continuación, recorremos todas las líneas del texto y las dibujamos. Fíjate que
después de dibujar una línea, se añade a posy el valor de la altura de la línea calculada
para que la siguiente se dibuje debajo, en el lugar correcto. Y, finalmente, se vuelca
todo en pantalla para que lo visualice el jugador.
función procesar()
En un juego real esta función sería bastante más complicada y extensa y ésta es la razón
por lo que se implementa como una función externa y no como un método más de la
clase Juego. De hecho, para poder modificarlo, juego se pasa como argumento a la
función.
Nuestro procesar() simplemente comprueba cuál es la imagen de fondo actual (es decir,
juego.image) para actuar convenientemente. Si se trata de castillo sólo haremos caso
cuando la frase que ha escrito el jugador sea ‘norte’; entonces cambiamos la imagen a
patio y la descripción a frase2. Algo similar ocurre cuando la imagen actual es patio; la
única salida es al sur y sólo se obedece en tal caso. Por otra parte, si el jugador escribe
‘salir’ el juego debe terminar.
Lo dicho, en un juego serio aquí se harían muchas comprobaciones. Además, nadie nos
impide añadir más atributos a la clase Juego para que se almacene más información sobre
el jugador (objetos que lleva, salud, armas, etc). Recuerda que en el juego ‘TheHouse’, el
parser era bastante largo y usaba bastantes variables de estado.
Cuerpo Principal del Juego
Llegamos al último bloque del juego. Para empezar, cargamos las imágenes de fondo
castillo y patio y definimos las descripciones de ambas localizaciones, frase1 y frase2.
En un juego completo, lo ideal sería una lista o un diccionario y no variables sueltas.
Continuamos definiendo los tipos de letra para la descripción de los lugares y las
situaciones (letraNarrar) y para el diálogo con el ordenador (letraHablar). También se
definen las zonas donde se van a mostrar con sendos Rect (dondeTexto y
dondeComando).
Justo antes del bucle del juego se crean los objetos que lo controlan; narrar, hablar y
juego. En la creación de este último se usan los dos anteriores. Observa que para
inicializar el juego necesitamos proporcionarle los datos de por donde empezar. La imagen
de fondo se pasa al crearlo (castillo) y la descripción del lugar se pone a mano con
narrar.texto = frase1
Finalmente, el bucle del juego realiza una y otra vez las dos tareas obvias; mostrar en
pantalla la situación actualizada y esperar las acciones del usuario con juego.update() y
procesar lo que haya introducido el jugador con procesar(juego).
PÁGINA 6 DE 10
CC: FERNANDO SALAMERO
7. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------
# aventura.py
# (cortaTexto basado en un script de Tiny Ducks On Wheels)
#-------------------------------------------------------------------
import pygame, sys
from pygame.locals import *
AMARILLO = (200,200,0)
BLANCO = (200,200,200)
VERDE = (0,100,0)
LILA = (50,0,50)
pygame.init()
visor = pygame.display.set_mode((800,600))
#-------------------------------------------------------------------
# Clase Juego
# (Clase principal que coordina las demás)
#-------------------------------------------------------------------
class Juego:
def __init__(self, surface, imagen, narrar, hablar):
self.surface = surface
self.image = imagen
self.narrar = narrar
self.hablar = hablar
def update(self):
self.surface.blit(self.image, (0,0))
self.narrar.update(10)
self.escuchar()
def escuchar(self):
maximo = self.hablar.rect.w / self.hablar.tipoLetra.size('a')[0]
self.hablar.frase = u''
self.hablar.cuadrado()
pygame.display.update()
hablando = True
while hablando:
for evento in pygame.event.get():
if evento.type == QUIT:
pygame.quit()
sys.exit()
elif evento.type == KEYDOWN:
PÁGINA 7 DE 10
CC: FERNANDO SALAMERO
8. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
if evento.key == K_RETURN:
hablando = False
elif evento.key == K_BACKSPACE:
self.hablar.frase = self.hablar.frase[:-1]
self.hablar.update(u'')
elif len(self.hablar.frase) < maximo:
self.hablar.update(evento.unicode)
#-------------------------------------------------------------------
# Clase Hablar
# ( Gestiona ka introducción de texto del usaurio)
#-------------------------------------------------------------------
class Hablar:
def __init__(self, surface, rect, tipoLetra, colorTexto, colorFondo):
self.surface = surface
self.rect = rect
self.tipoLetra = tipoLetra
self.color = colorTexto
self.colorFondo = colorFondo
self.margen = 10
self.frase = u''
def cuadrado(self):
cuadrado = pygame.Surface((self.rect.w,self.rect.h))
cuadrado.convert_alpha()
cuadrado.set_alpha(200)
cuadrado.fill(self.colorFondo)
self.surface.blit(cuadrado, self.rect.topleft)
def update(self, tecla):
x = self.rect.left + self.margen
y = self.rect.top + self.margen
self.frase += tecla
self.cuadrado()
self.surface.blit(self.tipoLetra.render(self.frase, True, self.color), (x,y))
pygame.display.update()
#-------------------------------------------------------------------
# Clase Narrar
# ( Muestra las descripciones de las situaciones)
#-------------------------------------------------------------------
class Narrar:
def __init__(self, surface, rect, tipoLetra, colorTexto, colorFondo):
self.surface = surface
self.rect = rect
self.tipoLetra = tipoLetra
self.color = colorTexto
PÁGINA 8 DE 10
CC: FERNANDO SALAMERO
9. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
self.colorFondo = colorFondo
self.texto = u''
def cortaTexto(self, anchoTotal):
lineas = self.texto.split("n")
resultado = []
for linea in lineas:
palabras = linea.split(" ")
comienzo = 0
i=0
while i < len(palabras):
ancho = self.tipoLetra.size(" ".join(palabras[comienzo:i+1]))[0]
if ancho > anchoTotal:
resultado.append(" ".join(palabras[comienzo:i]))
comienzo = i
i -= 1
elif i is len(palabras) - 1:
resultado.append(" ".join(palabras[comienzo:i + 1]))
i += 1
return resultado
def update(self, margen):
posx = self.rect.left + margen
lineas = self.cortaTexto(self.rect.width - 2*margen)
alturaLinea = self.tipoLetra.get_linesize()
posy = self.rect.top + (self.rect.h - alturaLinea * len(lineas)) / 2
cuadrado = pygame.Surface((self.rect.w, self.rect.h))
cuadrado.convert_alpha()
cuadrado.set_alpha(200)
cuadrado.fill(self.colorFondo)
self.surface.blit(cuadrado, (self.rect.left,self.rect.top))
for linea in lineas:
self.surface.blit(self.tipoLetra.render(linea, True, self.color), (posx, posy))
posy = posy + alturaLinea
pygame.display.update()
#-------------------------------------------------------------------
# Función procesar()
# ( El parser del juego)
#-------------------------------------------------------------------
def procesar(juego):
if juego.image == castillo and juego.hablar.frase == 'norte':
juego.image = patio
juego.narrar.texto = frase2
elif juego.image == patio and juego.hablar.frase == 'sur':
PÁGINA 9 DE 10
CC: FERNANDO SALAMERO
10. PROGRAMA: AVENTURA
CURSO: 1º BACHILLERATO
juego.image = castillo
juego.narrar.texto = frase1
elif juego.hablar.frase == 'salir':
pygame.quit()
sys.exit()
#-------------------------------------------------------------------
# Cuerpo Principal del Juego
#-------------------------------------------------------------------
castillo = pygame.image.load('castillo.jpg').convert()
patio = pygame.image.load('patio.png').convert()
frase1 = u'Abres los Ojos...nNo recuerdas nada. ¿Dónde estás? Al norte hay una
puerta.'
frase2 = u'Un patio se abre ante ti. Al sur está la puerta que has traspasado.'
letraHablar = pygame.font.Font('Grandezza.ttf', 24)
letraNarrar = pygame.font.Font('Adolphus.ttf', 24)
dondeTexto = pygame.Rect((100,300,600, 200))
dondeComando = pygame.Rect((100,550,600,40))
narrar = Narrar(visor, dondeTexto, letraNarrar, AMARILLO, LILA)
hablar = Hablar(visor, dondeComando, letraHablar, BLANCO, VERDE)
juego = Juego(visor, castillo, narrar, hablar)
narrar.texto = frase1
while True:
juego.update()
procesar(juego)
PÁGINA 10 DE 10
CC: FERNANDO SALAMERO