¿Cómo hacer un Pong?

“Programando con pilas-engine”

_images/pilas-logo.png

Índice:

Introducción

Este tutorial se creó para enseñar algunos pasos básicos en la creación de un juego 2d, usando pilas-engine (para ver como se instala, hacé click acá). Además, en el transcurso del mismo, vamos a hacer nuestro propio juego, el “Pong”.

Para empezar a usar pilas-engine, podemos ver el manual de uso (o descargarlo), para tener algunas nociones básicas. También se puede consultar a este resumen de pilas. En este caso empezaremos viendo qué son los actores y los fondos.

Actores y fondos

Actores

Un concepto importante en pilas es del de actores.

Actor:

Un actor en pilas es un objeto que aparece en pantalla, tiene una posición determinada y se puede manipular.

Por ejemplo:

  • Una nave.
  • Un ingeniero.
  • Una zanahoria.
_images/pilas-actores.png

Para empezar, pilas se puede usar directamente desde un intérprete interactivo de python. Una vez dentro del intérprete, tienes que escribir estas dos líneas de código:

1
2
import pilas         # importa la librería pilas
pilas.iniciar()      # inicia una ventana predeterminada

En tu pantalla tiene que aparecer una ventana de color gris.

Para ver un ejemplo de actor podemos ingresar (luego de haber generado la vetana con las líneas anteriores):

1
bomba = pilas.actores.Bomba()         # le asignamos a "bomba" el actor "Bomba()", ubicado en pilas.actores.

Se va a crear inmediatamente una bomba, en la ventana, como ésta:

_images/pilas-bomba.png

Como Bomba es un actor, encontraremos mucha funcionalidad en él que la tendrán el resto de los actores.

Por ejemplo:

Posición

Podemos cambiar la posición del actor en el eje de cordenadas mediante las propiedades x e y:

1
2
bomba.x = 100
bomba.y = 100

Se puede observar si pulsas la tecla F12, la posición de los actores en el eje, de esta forma:

_images/pilas-bomba100-100.png
Escala

Este atributo indica su tamaño en pantalla:

1
bomba.escala = 2         # le duplicamos el tamaño a "bomba"
_images/pilas-bomba-duplicada.png
Rotación

La rotación siempre se indica en grados, e indica el grado de inclinación hacia la derecha:

1
bomba.rotacion = 45
_images/pilas-bomba-rotada.png
Eliminar actor

Para eliminar un actor basta con llamar a la función eliminar():

1
bomba.eliminar()

Fondos

Otro concepto a aprender es el de fondos.

Fondo:

En pilas un fondo es un concepto muy simple, es solamente una imágen detras de mis actores, que suele ser un paisaje.

Por ejemplo:

1
fondo = pilas.fondos.Tarde()
_images/pilas-fondo.png

Bueno, después de esta primera parte ya vamos a poder probar muchas cosas, pero ya que esta guía está destinada a un determinado juego, para conocer más y poder aplicarlo consulten al manual.

A partir de ahora solo vamos a ver lo necesario para nuestro “Pong”.

Escenas: menú y controles

“Salir”

Ahora que ya tenemos un menú bien hecho podemos seguir con las funciones que implica. Empecemos con la más fácil, “Salir”:

1
2
def selecciona_salir():
    pilas.terminar()       # utilizamos esta funcion de pilas para cerrar nuestra ventana

Escenas

La siguiente función va a estar destinada a los controles. Para poder hacerla hay que empezar a trabajar con escenas.

Escena:

Las escenas en pilas son las partes de un juego, cuando termina una escena se borran todos sus actores y comienza una nueva con actores nuevos.

Cuando ponemos pilas.iniciar() se genera una escena llamada Normal, esta escena no tiene un comportamiento muy elaborado, simplemente imprime toda la pantalla de gris para que podamos colocar actores sobre ella y veamos una escena limpia.

Para generar una escena simplemente hay que meter todos sus actores y fondos en una clase, al ejecutar cada una de ellas automáticamente se borrarán los actores y fondos de escenas anteriores.

Veamos un ejemplo:

1
2
3
4
5
6
7
class PantallaBienvenida(pilas.escenas.Escena):      # definimos una clase para la escena
    def __init__(self):
        pilas.escenas.Escena.__init__(self)
        fondo = pilas.fondos.Pasto()      # generamos un fondo
        texto = pilas.actores.Texto('Bienvenido a pilas!!!')      # generamos un actor texto

PantallaBienvenida()      # ejecutamos la clase (escena)

Se nos va a crear algo como esto:

_images/pilas-ejemplo-escena.png

Pero si generamos otra clase, se va a reiniciar la escena:

1
2
3
4
5
6
7
class PantallaBienvenida2(pilas.escenas.Escena):      # definimos una clase para la escena
    def __init__(self):
        pilas.escenas.Escena.__init__(self)
        fondo = pilas.fondos.Tarde()      # generamos un fondo
        texto = pilas.actores.Texto('Entraste a pilas!!!')      # generamos un actor texto

PantallaBienvenida2()      # ejecutamos la clase (escena)

Y va a pasar algo como esto:

_images/pilas-ejemplo-escena.gif

Así como cada actor tiene su lugar en “pilas.actores.*” , para definir una escena hace falta pasarle de parámetro a la clase, “pilas.escenas.Escena”, eso hará que la clase sea una escena y no un actor. Más adelante veremos como hacer clases para cada actor.

Escena controles

Para continuar con la escena controles, yo usé de fondo la siguiente imagen:

_images/pilas-fondo-ayuda.png

Note

Acuérdense que pueden cambiar las imágenes usadas, como también algunos detalles, para hacer su propia versión.

Siguiendo con el código de nuestro programa, habrá que implementar una escena nueva, por lo tanto habrá que crear una clase:

1
2
3
4
5
6
7
8
9
class Escena_controles(pilas.escenas.Escena):      # definimos la clase Escena_controles (Escena)
    def __init__(self):
        pilas.escenas.Escena.__init__(self)
        fondo = pilas.fondos.Fondo('data/fondo_ayuda.png')      # le damos un fondo
        titulo = pilas.actores.Texto('Controles', magnitud=30, y=200)      # colocamos el titulo
        titulo.color = pilas.colores.rojo      # le damos un color
        texto1 = pilas.actores.Texto('Jugador 1 (rojo)', y=120, x=-120)     # colocamos subtitulo
        texto2 = pilas.actores.Texto('Jugador 1 (rojo)', y=120, x=120)     # colocamos subtitulo
        pilas.avisar('Pulsa ESC para regresar al menu')

Lo que hicimos fue simplemente declarar una clase para crear una escena nueva, le dimos un fondo y dos títulos simbolizando a cada jugador.

Si todo salió bien, debería quedar así:

_images/pilas-escena-ayuda.png

Bueno, habiendo terminado estas dos funciones, solo nos queda ver la que está vinculada a la escena del juego.

Para continuar, debemos tener en cuenta que esta escena es la mas extensa y complicada, ya que es donde interactúan todos los actores del juego.

Escena juego

Clases de actores

Vamos a empezar creando los actores con sus distintas propiedades y habilidades, para eso vamos a crear un archivo aparte llamado “actores.py”. Dentro de él haremos una clase para cada actor.

Un ejemplo de clase para un actor sería:

1
2
3
4
5
6
class Zanahoria(pilas.actores.Zanahoria):
    def __init__(self, x=0, y=0):
        pilas.actores.Zanahoria.__init__(self, x=x, y=y)
        self.aprender(pilas.habilidades.Arrastrable)

Zanahoria(20, 100)

Esto quedaría así:

_images/pilas-ejemplo-actorConClase.png

Conociendo como es el sistema para definir actores en clases, continuamos con el código del “Pong”.

Para la pelota del juego decidimos usar una bomba (“pilas.actores.Bomba()”), que explota al ganar uno de los dos jugadores:

1
2
3
4
5
6
7
8
class Pelota(pilas.actores.Bomba):      # representa la pelota del juego
    def __init__(self, x=0, y=0):
        pilas.actores.Bomba.__init__(self, x=x, y=y)
        self.circulo(x, y) = pilas.fisica.Circulo(x, y, 20, restitucion=1, friccion=0, amortiguacion=0)   # creamos un circulo de fisica con sus propiedades
        self.imitar(self.circulo)      # hacemos que la pelota siga los movimientos del circulo de fisica
        self.dx = 1      # creamos coeficiente de impulso para x
        self.dy = 1      # creamos coeficiente de impulso para y
        self.circulo.impulsar(self.dx * 50000, self.dy * 50000)      # impulsamos por primera vez la pelota para que arranque el juego

Los actores que usaremos en nuestro programa serán:

  1. Jugador 1
  2. Jugador 2
  3. Puntajes
  4. Pelota

Teniendo ya creada la pelota del juego, continuaremos con los jugadores. Las imágenes usadas para la creación de los mismos son:

_images/pilas-BaseRoja.bmp _images/pilas-BaseAzul.bmp

Comenzando con el jugador 1, haremos:

1
2
3
4
5
6
7
class Jugador1(pilas.actores.Actor):      # definimos la clase del jugador 1
    def __init__(self, x=0, y=0):
        pilas.actores.Actor.__init__(self, x=x, y=y)
        self.imagen = pilas.imagenes.cargar('BaseRoja.bmp')      # le cargamos la imagen
        self.radio_de_colision = 15      # le asignamos el radio de colision al actor
        self.aprender(pilas.habilidades.SeMantieneEnPantalla)      # le enseñamos a mantenerse en pantalla con esta habilidad
        self.aprender(self.MoverseConWS)      # le enseñamos a moverse con W y S, con una habilidad creada por nosotros (explicada mas adelante) dentro de la clase Jugador1

Basicamente, lo que hicimos fue cargar una imagen (llamada “BaseRoja.bmp” en este caso), asignarle un radio de colisión, obligarlo a mantenerse en pantalla y brindarle movimiento con las teclas W y S.

Para el jugador 2 es basicamente lo mismo, salvo que en vez de aprender a moverse con W y S, se mueve con las teclas Arriba y Abajo.

A continuación les mostramos el código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Jugador2(pilas.actores.Actor):      # definimos la clase del jugador 2
    def __init__(self, x=0, y=0):
        pilas.actores.Actor.__init__(self, x=x, y=y)
        self.imagen = pilas.imagenes.cargar('BaseAzul.bmp')      # le cargamos la imagen
        self.radio_de_colision = 15      # le asignamos el radio de colision al actor
        self.aprender(pilas.habilidades.SeMantieneEnPantalla)      # le enseñamos a mantenerse en pantalla con esta habilidad
        self.aprender(self.MoverseConArribaAbajo)      # le enseñamos a moverse con las teclas arriba y abajo (habilidad definida por nosotros)

    class MoverseConArribaAbajo(pilas.habilidades.Habilidad):      # esta clase define la habilidad llamada anteriormente
        def __init__(self, receptor):
            pilas.habilidades.Habilidad.__init__(self, receptor)
            pilas.eventos.actualizar.conectar(self.pulsa_tecla)      # le decimos que actualice la funcion self.pulsa_tecla

        def pulsa_tecla(self, evento):
            velocidad = 5
            c = pilas.mundo.control

            if c.arriba:    # si se ha pulsado arriba
                self.receptor.y += velocidad     # movemos al receptor (actor) para arriba
            if c.abajo:     # si se ha pulsado abajo
                self.receptor.y -= velocidad     # movemos al receptor (actor) para abajo

Por último, necesitamos puntajes, para esto deberán definir dos clases muy simples:

1
2
3
4
5
6
7
8
9
class Puntaje1(pilas.actores.Puntaje):      # definimos la clase del puntaje 1
    def __init__(self, x=-70, y=220):
        pilas.actores.Puntaje.__init__(self, x=x, y=y)
        self.color = pilas.colores.blanco      # le cargamos el color

class Puntaje2(pilas.actores.Puntaje):      # definimos la clase del puntaje 2
    def __init__(self, x=70, y=220):
        pilas.actores.Puntaje.__init__(self, x=x, y=y)
        self.color = pilas.colores.blanco      # le cargamos el color

Habilidades

Habilidad:

Una habilidad tambien es una clase, que está aplicada a un actor, y que no se ve por si sola.

La habilidad le enseña comportamientos al actor para diferenciarse de otros de su misma especie.

Por ejemplo, si creamos un mono llamado mono y le enseñamos la habilidad “pilas.habilidades.Arrastrable”:

1
2
mono = pilas.actores.Mono()      # creamos al mono
mono.aprender(pilas.habilidades.Arrastrable)     # le enseñamos que sea arrastrable

Este mono será distinto a los demás ya que se puede arrastrar con el mouse, en este caso la habilidad que usamos ya estaba creada en pilas, pero si queremos que un actor haga cosas diferentes y no encontramos una habilidad adecuada, podemos crear nuestras propias habilidades.

Para crear habilidades, lo único que hay que tener en cuenta es que la clase no va a ser de tipo “pilas.actores.Actor”, ni “pilas.escenas.Escena”, sino que va a ser una habilidad de tipo “pilas.habilidades.Habilidad”.

Comencemos con el jugador 1 o jugador rojo, para crearlo necesitamos crear una nueva habilidad que le va a enseñar a nuestro actor a moverse con las teclas <W> y <S>, para ir arriba y abajo.

A continuación veremos un ejemplo sencillo de habilidad creada por nosotros y aplicable a nuestro programa, la cual debe estar definida dentro de la clase Jugador1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pilas.simbolos import *

class MoverseConWS(pilas.habilidades.Habilidad):      # definimos la habilidad para que se mueva con W y S
    def __init__(self, receptor):      # ademas de su clase necesita saber cual es su receptor (el actor)
        pilas.habilidades.Habilidad.__init__(self, receptor)
        self.w = False      # esta variable sera True cuando se pulse la tecla w
        self.s = False      # esta variable sera True cuando se pulse la tecla s
        pilas.eventos.actualizar.conectar(self.pulsa_tecla)      # le decimos que actualice 'pulsa_tecla'
        pilas.eventos.pulsa_tecla.conectar(self.cuando_pulsa_la_tecla)
        pilas.eventos.suelta_tecla.conectar(self.cuando_suelta_la_tecla)

    def pulsa_tecla(self, evento):      # esta funcion mueve el actor para arriba (si pulso w) o abajo (si pulso s)
        velocidad = 5
        if self.w:
            self.receptor.y += velocidad
        elif self.s:
            self.receptor.y -= velocidad

    def cuando_pulsa_la_tecla(self, evento):      # detecta si pulso una tecla y cambia el estado de las variables
        self.procesar_cambio_de_estado_en_la_tecla(evento.codigo, True)

    def cuando_suelta_la_tecla(self, evento):      # detecta si solto una tecla y cambia el estado de las variables
        self.procesar_cambio_de_estado_en_la_tecla(evento.codigo, False)

    def procesar_cambio_de_estado_en_la_tecla(self, codigo, estado):     # recibe y cambia el estado
        mapa = {w: 'w', s: 's'}
        if mapa.has_key(codigo):
            setattr(self, mapa[codigo], estado)

Bueno, en esta clase podemos encontrar muchas cosas nuevas y que van a parecer difíciles de entender, pero las vamos a ver de nuevo, una por una, para saber como funcionan.

Empecemos con la función “pilas.eventos.actualizar.conectar()”. Lo único que hace esta función es recibir otra, definida dentro de la clase (por eso lleva “self.” adelante), y ejecutarla constantemente (actualizarla).

Por lo general, se usa para comprobar una condición que cambia su estado (puede ser “True” o “False”, en este caso) durante el desarrollo del juego.

Lo que hicimos dentro de esta función que pasamos como parámetro (“self.pulsa_tecla”), fue:

  1. Definir una velocidad.
  2. Comprobar si se ha pulsado w, si así fue, sumar 5 (la velocidad) a la posición dentro de la coordenada “y” (hacia arriba) del actor (receptor).
  3. De lo contrario, si se pulso s, en este caso, restar la velocidad a la posición “y” del actor (para que este vaya hacia abajo)

Para saber si se ha pulsado o soltado una de esas teclas usamos las variables “self.w” y “self.s”, pero necesitamos que cambien su estado (valor: “True” o “False”), dependiendo de la interacción del usuario. Para esto, utilizamos la siguiente funciones: “pilas.eventos.pulsa_tecla.conectar()” y”pilas.eventos.suelta_tecla.conectar()”; que como nos dicen sus nombres, detectan la pulsación y despulsación de alguna tecla.

Warning

Hay que saber distinguir entre la función: “self.pulsa_tecla()”, definida por nosotros en la clase (por eso el “self.”); y la otra función totalmente distinta: “pilas.eventos.pulsa_tecla.conectar()”, definida en el módulo pilas y llamada por nosotros.

Volviendo con las funciones para detectar la pulsación de teclas, las dos tienen algo en común: llaman a otra función (creada por nosotros), “self.procesar_cambio_de_estado_en_la_tecla()”, que recibe un codigo (la tecla que se pulsó) y un estado (“True” si se pulsó, “False” si se soltó). Esta función lo que hace es:

  1. Crear un mapa con las posibilidades de teclas que se puedan haber pulsado (en este caso solo nesecitamos W y S).

  2. Preguntar si el codigo recibido pertenece a alguna opción del mapa (“mapa.has_key(codigo)”, ¿mapa contiene una clave <W o S según sea>?).

  3. Y por último usa una función de python llamada “setattr()”, que recibe tres parámetros y funciona más o menos así:

    1
    2
    >>> setattr(parametro1, parametro2, parametro3)
    parametro1.parametro2 = parametro3
    

    Lo que hace es simplemente setear atributos, como lo dice el nombre. Se le pasa un 1° parámetro que va a ser la clase, el 2° que va a ser el atributo y el 3° que va ser el valor que se le asigne.

    Para terminar de entenderlo mejor veamos nuestro caso, haciendo de cuenta que la tecla pulsada (si es pulsada la tecla, el estado va a ser “True”) fue W:

    1
    2
    >>> setattr(self, mapa[codigo], estado)
    self.w = True
    

    Ahora se entiende por que declaramos las variables “self.w” y “self.s”. Para conocer el estado de cada una en la función “self.pulsa_tecla()”.

Habiendo creado ya las distintas escenas con sus respectivos fondos y actores. Nuestro juego está casi terminado, solo nos quedaría crear la parte de las colisiones (para que cuando la pelota toque un lado, se sume un punto en el puntaje; y para que a determinada cantidad de puntos explote la bomba y termine el juego).

En este momento nuestro “Pong” se vería así:

_images/pilas-pong-terminado.gif

Colisiones

La parte de colisiones aun no está desarrollada, este tutorial será actualizado muy pronto.

Note

Recuerden que los puntajes deben cambiar su valor a medida que tocan los laterales; así como también, que deben crear una colisión para que la bomba rebote en las dos barras.

Acá se encuentra el código del “Pong”, junto con las imágenes usadas y este tutorial.

Cualquier contribución será bien aceptada, ¡Suerte!