Este documento describe un juego de conducción en el que el jugador controla un coche que se desplaza por una carretera simulada. La carretera está compuesta de dos imágenes de fondo que se desplazan verticalmente de forma continua para simular que es interminable. El documento explica las clases Coche, Planta y Fondo que representan los sprites del juego, así como variables, bucles y funciones para controlar los movimientos, colisiones y generación aleatoria de obstáculos a lo largo de la carretera.
MAYO 1 PROYECTO día de la madre el amor más grande
Drive: Un juego de Scrolling
1. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
Drive: Un juego de Scrolling
(Basado en un script original de Mark Ivey)
Importación de Librerías y Definición de Constantes
Se cargan las librerías habituales, incluyendo como en el juego anterior random y os, y
definimos la anchura y la altura de la ventana de juego. También definimos la constante
VELOCIDAD que nos va a indicar la velocidad a la que vamos a movernos por la
carretera (en realidad, la velocidad a la que va a desplazarse la imagen de carretera de
fondo hacia abajo para dar la sensación de movimiento; tal como lo hemos puesto, será a
base de 5 pixeles por fotograma)
Funciones cargarImagen() y cargarSonido()
Las funciones cargarImagen() y cargarSonido() vuelven a ser las mismas que en el
juego anterior, sólo hemos modificado ligeramente la primera de ellas. ¿De qué manera?
Hemos asumido que no siempre queremos transparencia. Así que el segundo parámetro de
la función, tieneTrasnparencia lo que va a indicar es si la imagen ya la tiene
incorporada o no (por defecto es no). Comprobando con un if cuál es el caso, llamamos a
PÁGINA 1 DE 10
CC: FERNANDO SALAMERO
2. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
la función adecuada para convertir la imagen, ya sea imagen.convert_alpha() o
imagen.convert(), viejas conocidas nuestras.
Excepto la imagen de fondo que vamos a desplazar, las demás imágenes son archivos png
con transparencia, así que en esos casos invocaremos a la función con True.
Clase Coche
Ya sabemos, una vez vistos los tutoriales anteriores, cuál es la estructura de la definición de
un sprite de PyGame, es decir, una clase derivada de pygame.sprite.Sprite. Vamos a
centrarnos en las variaciones propias que nos interesan para este juego. La clase Coche
será la que represente el coche del jugador. Fíjate que, en este juego, lo que se desplaza
verticalmente es la imagen de fondo y no el coche.
1. En __init__(), aparte de definir los atributos necesarios y situar en pantalla al sprite,
creamos la variable giro. Para que el movimiento parezca real, necesitamos girar un
poco el coche cuando el jugador pulse izquierda o derecha. De ello es precisamente de
lo que se va a encargar esta variable. Hay un detalle importante al respecto; habrás
notado que nuestra clase posee dos atributos que almacenan una imagen, image (el
obligatorio de todo sprite) e imagen_base. ¿Por qué?
Cuando simplemente desplazamos una imagen por pantalla, no la deformamos y todo
está en orden. Pero cuando se modifica su forma o su tamaño estamos en lo que se
conoce como transformación destructiva. Si realizas varias de estas
transformaciones, tu imagen final puede no parecerse en nada a la que tenías
originalmente. Un ejemplo; tienes una imagen y la conviertes en una que es tres veces
más pequeña. Luego coges ésta y la amplías tres veces para volver al tamaño original.
¡La verás pixelada! Por el camino, has perdido la información de esos pixeles que han
desaparecido al encoger la imagen. Al ampliarla nuevamente, el ordenador se ha
tenido que inventar esa información dándote la imagen modificada.
La forma de solucionar este problema es tener almacenada la imagen original y utilizar
ésta en cada transformación, sin acumular transformaciones destructivas. Como
quiera que la rotación de una imagen es destructiva (el número de pixeles de la
pantalla es limitado y hay que hacer encajar la imagen en éstos), almacenamos en
imagen_base el dibujo original del coche. Cuando tengamos que modificarlo
hacemos sobre él las operaciones necesarias y el resultado lo ponemos en el image del
sprite para que se muestre correctamente.
2. Lo más importante ocurre en update(). Para empezar, no queremos que el coche gire
sin parar y pueda darse la vuelta. Sólo queremos permitir el giro para avanzar por la
carretera y nada más. Teniendo en cuenta que el ángulo de giro al comienzo es 0,
vamos a dejar que el coche se desvíe un máximo de 45 grados a ambos lados. El
bloque if que encontramos al comienzo de la función tiene este objetivo.
Por otra parte, si no hemos llegado a ese límite (entramos en el else) sí que podemos
girar el coche a la posición deseada. Debes también tener en cuenta que, al girar un
sprite, PyGame mantiene fija la esquina superior derecha del sprite. Esto tiene
ventajas e inconvenientes. El inconveniente que nos ocupa ahora es que, como al girar
la imagen ésta cambia de tamaño, el giro queda artificial (el centro del rect
desplazado). ¿Cómo solucionarlo? Si piensas un poco darás con la solución; primero
almacenamos las coordenadas del centro del rect
x, y = self.rect.center
y más adelante, una vez hecho el giro, devolvemos el centro a su sitio
PÁGINA 2 DE 10
CC: FERNANDO SALAMERO
3. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
self.rect = self.image.get_rect()
self.rect.center = x,y
La primera de las dos líneas anteriores no es gratuita. Recuerda que el rect del dibujo
ha cambiado de tamaño, así que primero hay que ajustar el rect del sprite al nuevo
tamaño con get_rect() y luego sí, recolocar su rect.center.
Fuera de ello, el giro en sí mismo se realiza de la siguiente manera:
self.image = pygame.transform.rotate(self.imagen_base, self.giro)
Observa que a la función de PyGame pygame.transform.rotate() hay que pasarle
dos argumentos; el primero es la imagen y la segunda el ángulo de giro que se quiere
aplicar. Ese ángulo es como en matemáticas; positivo en sentido antihorario y
negativo en el sentido de las agujas del reloj. El resultado de la función es la imagen
girada hasta el ángulo deseado, por lo que pasamos a almacenarla en el atributo
image del sprite.
No ha acabado aquí todo lo correspondiente al movimiento del coche. Si se trata de
una simulación realista, cuando el coche está girado debe desplazarse hacia el lado
indicado. Y debe hacerlo mientras esté en movimiento. Ese es el sentido de
if self.giro and scroll:
scroll es una variable de estado booleana que indica si el coche está en marcha
(como veremos luego, mientras tengas pulsada la barra espaciadora el coche se
moverá). Por otra parte, ya hemos visto que si una variable cualquiera se usa en un if
funciona como False cuando tiene un valor nulo y como True en cualquier otro
caso. Así que nuestro if se cumplirá cuando se den ambas condiciones a la vez, es
decir, el coche esté en movimiento y su ángulo de giro sea distinto de cero (que es lo
que queremos).
El truco usado para mover el coche en la dirección adecuada es el siguiente:
self.rect.move_ip(-self.giro/10,0)
Para entenderlo debes tener en cuenta que, por ejemplo, para moverse hacia la
izquierda, la coordenada horizontal debe disminuir mientras que el ángulo
de inclinación del coche será positivo. Lo contrario ocurre en el movimiento hacia
la derecha. Por ello, en la conocida función move_ip(), el valor que le damos al
desplazamiento de la primera coordenada tiene el signo cambiado respecto al
ángulo de giro. ¿Qué valor concreto ponemos? Eso ya depende de lo deprisa que
quieras que se desplace. Hemos puesto una cantidad en números redondos dividiendo
por 10 el giro (así, por poner un caso, cuando el ángulo de es 45 grados, el coche se
desplazará horizontalmente a razón de 4 pixeles por fotograma). Recuerda que no hay
nada en contra del método de prueba y error; cambia el valor hasta que encuentres
uno que te parezca razonable.
El resto es simplemente la comprobación habitual para que el sprite no se salga de los
límites de la ventana.
PÁGINA 3 DE 10
CC: FERNANDO SALAMERO
4. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
Clase Planta
Los obstáculos que van apareciendo en el juego son sprites de tipo Planta. Veamos:
1. Para empezar, para dar un poco de variedad, usamos dos diseños distintos de arbusto.
Para que el programa elija al azar entre uno de los dos hemos usado un if asociado a
random.randint(0,1). Como ya sabes, esta función devuelve un número al azar, en
este caso un 0 o un 1. Según cuál sea de los dos (recuerda que un valor cero es
equivalente a un False), se utiliza como imagen del sprite una u otra. Fíjate que
conseguir que sigan saliendo al azar pero que haya más de un tipo que de otro,
también es fácil. Por ejemplo, si se hubiera elegido un número del 0 al 2, en promedio,
habría dos veces más de un tipo que del otro (con el valor 0 tendríamos planta2.png
y con los valores 1 y 2 planta1.png). Y, por cierto, como ambas imágenes son de tipo
png con trasnparencia, llamamos a la función cargarImagen() con el valor True.
El otro detalle de la función __init__() es que usa dos parámetros (a parte del
obligatorio self). Eso nos permitirá crear la planta en la posición que queramos ya que
luego los usamos para posicionar el rect del sprite con topleft.
2. Como las plantas son parte del paisaje, se han de mover con él. En la función
update() miramos si el coche está en marcha (en cuyo caso scroll será True) y
desplazamos consecuentemente el sprite en la dirección vertical (hacia abajo) la
cantidad indicada por VELOCIDAD (exactamente igual que el fondo).
Queda un detalle importante. Una vez que la planta pase de largo y salga por la parte
inferior de la pantalla no necesitaremos más el sprite. Para que no gaste recursos del
ordenador y no se redibuje una y otra vez fuera de la pantalla, conviene eliminarla con
self.kill().
Clase Fondo
La clase Fondo representa la imagen de la carretera que se va a ir desplazando hacia abajo
para dar la sensación de movimiento. El término técnico inglés que se da a este tipo de
imágenes es Tiles (algo así como cuadrículas, en español). La imagen no es de tamaño
infinito pero debe parecerlo, así que a medida que se termine hay que ir añadiendo a
continuación otra imagen o ella misma, dando así la sensación de que la carretera no se
acaba nunca. Veremos luego como implementar esto; de momento vamos a ver cómo está
definido este tipo especial de objetos (he usado una clase derivada de un sprite, pero hay
otras muchas formas de hacerlo).
1. La función __init__() es la misma que la de la clase Planta, incluso más sencilla
puesto que tenemos una sola imagen de carretera (de paso vemos que ampliar la
definición para tener varias y crear la sensación de que la carretera es siempre distinta
no es muy complicado entonces).
2. Respecto a update() hay que adelantar algo. En el juego vamos a usar dos imágenes
de carretera, una a continuación de la otra. Para simular que la carretera no se acaba, a
medida que una de ellas sale por completo de la pantalla se cambiará su posición de
forma que quede justo detrás de la que viene a continuación (como si fuera un rodillo).
Así que en update() miramos primero si hay movimiento con un bloque if y la
variable booleana de estado scroll y en tal caso se procede a desplazar el sprite.
Primero bajamos el sprite el número de pixeles que indica VELOCIDAD. En segundo
lugar hay que ver cuando poner el sprite detrás del segundo que vayamos a tener.
Piensa que los dos son iguales y tienen por lo tanto la misma altura. Cuando el borde
superior del primero llegue abajo el segundo habrá bajado ya esa cantidad, así que hay
PÁGINA 4 DE 10
CC: FERNANDO SALAMERO
5. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
que retroceder el sprite su altura menos la altura de la ventana. Así es como deben
empalmar:
if self.rect.top >= ALTO:
self.rect.bottom = ALTO - self.rect.height
Cuerpo Principal del Juego
Siguiendo el esquema habitual nos encontramos con lo siguiente:
1. Inicializar y cargar sonidos
Poco que comentar aquí. El sonido que cargamos en la variable choque lo
emplearemos luego cuando el coche colisione con algún arbusto.
2. Crear los Sprites y los Grupos
Creamos los dos sprites que se encargarán de simular la carretera interminable. El
primero, carretera1, lo situamos en pantalla ocupándola por completo. El segundo,
carretera2, lo creamos justo encima. Como en la definición de la clase Carretera los
parámetros que se usan para posicionar el sprite son los de la esquina superior
izquierda, en este segundo objeto hemos de poner de coordenada vertical la altura del
propio sprite (en este caso de carretera1, ya que es igual a carretera2 pero este
último ¡todavía no está creado!) cambiada de signo:
carretera2 = Carretera(0, -carretera1.rect.height)
A continuación creamos el coche del jugador, coche, y añadimos los sprites a los
grupos apropiados; los tiles a fondoGrupo y el coche a cocheGrupo. Creamos, así
mismo, un grupo inicialmente vacío pero que contendrá los arbustos que vayan
apareciendo en el juego al azar, cosasGrupo.
3. Definición de variables de útiles
Ha tocado el turno a la definición de las variables de juego (muchas veces también
llamadas variables de estado). jugando controla si el juego está activo o ha
terminado, por lo que lo emplearemos el el bucle while típico de la animación. scroll
ya sabemos que indica si el coche está en marcha o no. intervaloPlantas lo usamos
de la misma manera que usamos intervaloEnemigos en el juego de Star Wars;
vigila cada cuánto hay que crear un nuevo obstáculo. Igual que la otra vez, eso se
realizará cuando llegue a un cierto valor y se ponga a cero la variable para volver a
empezar. Finalmente, reloj nos permitirá, de la forma habitual, controlar la velocidad
de la animación.
4. Bucle del Juego
En el bucle del juego, después de indicar que se va a ejecutar a 60 fotogramas por
PÁGINA 5 DE 10
CC: FERNANDO SALAMERO
6. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
segundo, entramos directamente el la lista de eventos. Hay dos situaciones en las
que nos fijamos aquí. La primera es el momento de terminar la partida, ya sea al cerrar
la ventana o al pulsar la tecla ESC. La segunda es determinar cuando hay que poner el
coche en marcha o cuando hay que pararlo. La tecla que se encargará de ello es la de la
barra espaciadora (con ella pulsada el motor está en funcionamiento y cuando la
dejamos de pulsar el motor se detiene). Nada más sencillo; nos fijamos en los eventos
KEYDOWN y KEYUP y si la tecla culpable es K_SPACE y asignamos el valor
conveniente a la variable scroll.
Para mover el coche sin interferir a la barra espaciadora usamos, también como
siempre, la función pygame.key.get_pressed(). El diccionario resultante lo
almacenamos en teclasPulsadas y miramos si las teclas del cursor están
implicadas, en cuyo caso se procede a girar el coche en la dirección adecuada.
Recuerda el convenio de signos con los ángulos; al girar hacia la izquierda hay que
aumentar el ángulo (por eso sumamos VELOCIDAD al atributo giro del coche) y hay
que restar cuando el giro es hacia la derecha.
El código que encontramos luego es el mismo que en Star Wars; se incrementa el
contador y si llega a 200 se crea una nueva Planta. Fíjate cómo se ha hecho:
cosasGrupo.add( Planta( random.randint(0,ANCHO) , -50 ) )
Al crear el arbusto lo hacemos fuera de la pantalla (50 pixeles por encima) para que
aparezca suavemente por el borde superior mientras se mueve. Su coordenada
horizontal la generamos al azar en cualquier punto comprendido entre los límites de la
ventana.
Ya está casi todo hecho. Lo siguiente es llamar a la función update() de todos los
grupos para que sitúe a todos los sprites en las nuevas posiciones. Y antes de dibujarlos
en pantalla hay que comprobar si hay colisión entre el coche y algún arbusto. El
código es casi idéntico que el que usamos en Star Wars. Hay una pequeña diferencia;
pygame.sprite.groupcollide( cocheGrupo, cosasGrupo, 0, 1)
En el programa anterior, cuando un rayo láser impactaba en una nave, queríamos que
desaparecieran los dos sprites. Ahora lo que queremos es que desaparezca el
arbusto pero no el coche. Es por eso por lo que en los argumentos encargados de
eliminar a los sprites implicados en la colisión ponemos un 0 en el correspondiente al
grupo del coche (‘no lo elimines’) y un 1 en el grupo de los arbustos (‘elimínalo’).
Hecho el trabajo duro, sólo queda usar draw() con todos los grupos para que dibuje
los sprites en sus nuevas posiciones y pygame.display.update() para que se
vuelque el fotograma completado en pantalla.
Una última cosa; ¿te has fijado que esta vez no hemos borrado los sprites de sus
antiguas posiciones? En este caso no hace falta por que, al ocupar el tile de la carretera
toda la ventana, cuando se dibuja este primer sprite lo hace por encima de todo lo que
hubiera antes. Algo que nos ahorramos.
PÁGINA 6 DE 10
CC: FERNANDO SALAMERO
7. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------
# Clon de Drive
# (Basado en un script original de Mark Ivey)
#-------------------------------------------------------------------
import random
import os, pygame
from pygame.locals import *
ANCHO = 800
ALTO = 600
VELOCIDAD = 5
#-------------------------------------------------------------------
# Función cargarImagen()
# ( Carga una imagen desde un archivo, devolviendo el objeto apropiado)
#-------------------------------------------------------------------
def cargarImagen( archivo, tieneTransparencia = False ):
lugar = os.path.join( "data", archivo )
try:
imagen = pygame.image.load( lugar )
except pygame.error, mensaje:
print "No puedo cargar la imagen:", lugar
raise SystemExit, mensaje
if tieneTransparencia:
imagen = imagen.convert_alpha()
else:
imagen = imagen.convert()
return imagen
#-------------------------------------------------------------------
# Función cargarSonido()
# ( Carga un sonido desde un archivo, devolviendo el objeto apropiado)
#-------------------------------------------------------------------
def cargarSonido( archivo ):
class sinSonido:
def play( self ):
pass
if not pygame.mixer or not pygame.mixer.get_init():
return sinSonido()
PÁGINA 7 DE 10
CC: FERNANDO SALAMERO
8. PROGRAMA: DRIVE
CURSO: 1º BACHILLERATO
lugar = os.path.join( "data", archivo )
try:
sound = pygame.mixer.Sound( lugar )
except pygame.error, message:
print "No puedo cargar el sonido:", lugar
raise SystemExit, message
return sound
#-------------------------------------------------------------------
# Sprite Coche
# (Coche del Jugador)
#-------------------------------------------------------------------
class Coche( pygame.sprite.Sprite ):
def __init__( self ):
pygame.sprite.Sprite.__init__( self )
self.imagen_base = cargarImagen( "coche.png", True )
self.image = self.imagen_base
self.rect = self.image.get_rect()
self.giro = 0
self.rect.center = (550,500)
def update( self ):
if self.giro >= 45:
self.giro = 45
elif self.giro <= -45:
self.giro = -45
else:
x, y = self.rect.center
self.image = pygame.transform.rotate(self.imagen_base, self.giro)
self.rect = self.image.get_rect()
self.rect.center = x,y
if self.giro and scroll:
self.rect.move_ip(-self.giro/10,0)
if self.rect.left <= 0:
self.rect.left = 0
elif self.rect.right >= ANCHO:
self.rect.right = ANCHO
#-------------------------------------------------------------------
# Sprite Planta
# (Palntas de la carretera)
#-------------------------------------------------------------------
class Planta( pygame.sprite.Sprite ):
PÁGINA 8 DE 10
CC: FERNANDO SALAMERO