Aventura

10
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. PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO PÁGINA 1 DE 10 CC: FERNANDO SALAMERO

description

 

Transcript of Aventura

Page 1: Aventura

Aventura: Ficción Interactiva

(Parte del código de recorte basado en un script original de Tiny Ducks On Wheels)

IntroducciónEl 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 NarrarUn 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 HablarEl 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.

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 1 DE 10 CC: FERNANDO SALAMERO

Page 2: Aventura

3. La clase JuegoSe 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:

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 2 DE 10 CC: FERNANDO SALAMERO

Page 3: Aventura

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 JUEGO1. 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''

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 3 DE 10 CC: FERNANDO SALAMERO

Page 4: Aventura

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 HablarSe 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.

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 4 DE 10 CC: FERNANDO SALAMERO

Page 5: Aventura

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 NarrarMuestra 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í:

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 5 DE 10 CC: FERNANDO SALAMERO

Page 6: Aventura

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 JuegoLlegamos 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).

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 6 DE 10 CC: FERNANDO SALAMERO

Page 7: Aventura

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

#-------------------------------------------------------------------# aventura.py# (cortaTexto basado en un script de Tiny Ducks On Wheels)#-------------------------------------------------------------------

import pygame, sysfrom 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:

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 7 DE 10 CC: FERNANDO SALAMERO

Page 8: Aventura

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

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 8 DE 10 CC: FERNANDO SALAMERO

Page 9: Aventura

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':

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 9 DE 10 CC: FERNANDO SALAMERO

Page 10: Aventura

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)

PROGRAMA: AVENTURA CURSO: 1º BACHILLERATO

PÁGINA 10 DE 10 CC: FERNANDO SALAMERO