Programar Juegos Arcade
con Python y Pygame

Chapter 8: Introducción a la Animación

8.1 El Rectángulo Saltarín

Vídeo: Rectángulo Saltarín

Para comenzar con nuestra primera animación, tomaremos la plantilla básica de programa Pygame del Capítulo 5, la cual abre una ventana en blanco. Esta plantilla pygame_base_template.py la podemos encontrar en:
ProgramArcadeGames.com/python_examples/f.php?file=pygame_base_template.py

Juntos construiremos un programa que rebote un rectángulo blanco alrededor de una ventana con fondo negro. Eres libre de escoger cualquier otro color, eso sí, asegúrate que el color del fondo sea distinto al del rectángulo!

fig.bouncing_rectangle

El primer paso es comenzar con la plantilla básica y cambiarle el color de fondo; de blanco a negro. Esa parte del código se encuentra alrededor de la línea 46.

pantalla.fill(NEGRO)

El paso siguiente es dibujar el rectángulo que pretendemos animar. Un rectángulo sencillo será suficiente. El código debería colocarse después de haber limpiado la pantalla, y antes de actualizarla.

pygame.draw.rect(pantalla, BLANCO, [50, 50, 50, 50])

En cada iteración del bucle, el rectángulo se dibujará en una ubicación (x, y) con valores (50, 50). Esto es controlado por los dos primeros números 50 de la lista. Hasta que estos números no cambien, el cuadrado no se moverá.

El rectángulo tendrá 50 píxeles de largo por 50 píxeles de alto. Las dimensiones son controladas por los dos últimos números de la lista. Podemos también llamar cuadrado a nuestro rectángulo, ya que tiene las mismas dimensiones de largo que de alto. Pero yo seguiré llamándolo rectángulo, porque todos los cuadrados son rectángulos, y dependiendo del monitor y la resolución utilizada, los píxeles no siempre son cuadrados. Busca el término Relación de Aspecto de Píxel si realmente estás interesado en el tema.

¿Cómo hacemos para ir cambiando nuestra ubicación en lugar de quedarnos detenidos en (50, 50)? ¡Usando una variable, por supuesto! El siguiente código es un paso hacia ese objetivo:

rect_x = 50
pygame.draw.rect(pantalla, BLANCO, [rect_x, 50, 50, 50])

Para mover el rectángulo hacia la derecha, podemos incrementar x por uno en cada fotograma. El siguiente código se aproxima a ello, pero no lo logra del todo:

rect_x = 50
pygame.draw.rect(pantalla, BLANCO, [rect_x, 50, 50, 50])
rect_x += 1

El problema con el código anterior es que rect_x se reinicia a 50 en cada iteración del bucle. Para solucionarlo, movemos la inicialización de rect_x fuera del bucle. La próxima parte del código desplazará correctamente el rectángulo hacia la derecha.

# Posición x de partida del rectángulo
# Observa que se encuentra fuera del bucle while principal.
rect_x = 50

# -------- Bucle Principal del Programa -----------
while not hecho:
	for evento in pygame.event.get(): # El usuario hizo algo
		if evento.type == pygame.QUIT: # Si el usuario hace click sobre cerrar
			hecho = True # Avisa de que hemos acabado, por lo que salimos de este bucle

	# Establece el fondo de pantalla
	pantalla.fill(NEGRO)

	pygame.draw.rect(pantalla, BLANCO, [rect_x, 50, 50, 50])
	rect_x += 1

Para mover más rápido a la caja, en lugar de incrementar rect_x por 1, hagámoslo por 5:

rect_x += 5

Podemos expandir este código para que tanto x como y se incrementen, y de esta forma conseguimos que el cuadrado se mueva tanto hacia abajo como hacia la derecha:

# Posición de partida del rectángulo
rect_x = 50
rect_y = 50

# -------- Bucle Principal del Programa -----------
while not hecho:
	for evento in pygame.event.get():
		if evento.type == pygame.QUIT: 
			hecho = True

	#  Establece el fondo de pantalla
	pantalla.fill(NEGRO)
	
	# Dibuja el rectángulo
	pygame.draw.rect(pantalla, BLANCO, [rect_x, rect_y, 50, 50])

	# Desplaza el punto de partida del rectángulo
	rect_x += 5
	rect_y += 5

El rumbo y la velocidad de las cajas se pueden almacenar en un vector. Esto facilita que ambos parámetros se puedan modificar fácilmente. El siguiente trozo de código nos muestra el uso de variables para almacenar el cambio en x e y (5, 5).

# Posición de partida del rectángulo
rect_x = 50
rect_y = 50

# Velocidad y rumbo del rectángulo
rect_cambio_x = 5
rect_cambio_y = 5

# -------- Bucle Principal del Programa-----------
while not hecho:
	for evento in pygame.event.get(): # El usuario hizo algo
		if evento.type == pygame.QUIT: # Si el usuario hace click sobre cerrar
			hecho = True # Avisa de que hemos acabado, por lo que salimos de este bucle

	# Establece el fondo de pantalla
	pantalla.fill(NEGRO)

	# Dibuja el rectángulo
	pygame.draw.rect(pantalla, BLANCO, [rect_x, rect_y, 50, 50])

	# Desplaza el punto de partida del rectángulo       
	rect_x += rect_cambio_x
	rect_y += rect_cambio_y

Cuando la caja llega al borde de la pantalla, continúa su camino sin que nada la detenga. Nada consigue que rebote al llegar a él. Para invertir el rumbo y que el rectángulo viaje hacia la derecha, necesitamos que rect_cambio_y cambie de 5 a -5 una vez que la figura llega al borde inferior de la pantalla. Sabemos que el rectángulo se encuentra en el fondo, cuando el valor de rect_y es mayor que la altura de la pantalla. El siguiente código se encarga de comprobarlo e invertir el rumbo:

# Rebota el rectángulo si es necesario
if rect_y > 450:
	rect_cambio_y = rect_cambio_y * -1
fig.450
Figure 8.1: Ubicación del rectángulo basada en la coordenada y

¿Por qué comprobar el valor de rect_y frente a 450? Si la pantalla tiene 500 píxeles de alto, comprobar su valor frente a 500 sería una opción lógica a primera vista. Pero recordemos que el rectángulo empieza a dibujarse desde la esquina superior izquierda del mismo. Si lo dibujáramos con inicio en 500, se dibujaría desde 500 hasta 550, saliéndose por completo de la pantalla antes de rebotar. Ver la Figura 8.1.

Si tenemos en cuenta que el rectángulo tiene 50 píxeles de alto, la ubicación correcta para que rebote es:
$500 - 50 = 450$.

El siguiente código rebotará al rectángulo por las cuatro paredes de una ventana de 700 x 400:

# Rebota al rectángulo si es necesario
if rect_y > 450 or rect_y < 0:
	rect_cambio_y = rect_cambio_y * -1
if rect_x > 650 or rect_x < 0:
	rect_cambio_x = rect_cambio_x * -1

Si te interesa algo más que solo un rectángulo, tienes varios comandos de dibujo que pueden aprovechar las variables rect_x y rect_y. El siguiente código dibuja un rectángulo rojo dentro del rectángulo blanco. El rectángulo rojo está desplazado 10 píxeles en las direcciones x,y, desde la esquina superior izquierda del rectángulo blanco. También es, 20 píxeles más pequeño en ambas direcciones. Esto produce un borde blanco de 10 píxeles de grosor alrededor del rectángulo rojo. Observa la Figura 8.2.

fig.two_part_rectangle
Figure 8.2: Rectángulo blanco con cuadrado rojo en el centro
# Dibuja un rectángulo rojo dentro del blanco
pygame.draw.rect(pantalla, BLANCO, [rect_x, rect_y, 50, 50])
pygame.draw.rect(pantalla, ROJO, [rect_x + 10, rect_y + 10 , 30, 30])

8.2 Nevando

¿No tienes suficiente con animar un elemento? ¿Quieres animar más objetos? ¿Qué tal animar cientos de ellos a la vez? Aquí te mostramos como usar las técnicas de la sección 8.1 para animar copos de nieve que caen.

8.2.1 Explicación del Código

Vídeo: Nevando

Para empezar, abre la plantilla que crea una ventana en blanco. Una vez más, el código pygame_base_template.py lo puedes encontrar aquí:
ProgramArcadeGames.com/python_examples/show_file.php?file=pygame_base_template.py

Es posible crear ubicaciones x,y para cosas como estrellas, nieve o lluvia usando números aleatorios. La forma más simple de hacerlo es utilizando un bucle for para dibujar círculos en posiciones aleatorias x,y. Intenta el siguiente código dentro del bucle while.

for i in range(50):
    x = random.randrange(0, 400)
    y = random.randrange(0, 400)
	pygame.draw.circle(pantalla, BLANCO, [x, y], 2)

Pruébalo. El programa tiene un problema singular. Veinte veces por segundo, cada vez que itera a través del bucle, dibuja la nieve en lugares aleatorios. Intenta ajustar el contador de copos de nieve para que veas como eso cambia la imagen.

Obviamente, queremos ubicar aleatoriamente los copos de nieve y mantenerlos en el mismo sitio. No queremos generar nuevas ubicaciones, veinte veces por segundo. Necesitamos mantener en una lista, el lugar donde se encuentran. Podemos usar una lista Python para conseguirlo. Lo siguiente debería ir antes del bucle principal. En caso contrario, el programa añadirá otros 50 copos de nieve a la lista cada 1/20 segundos.

for i in range(50):
    x = random.randrange(0, 400)
    y = random.randrange(0, 400)
    lista_nieve.append([x, y])

Una vez que hemos añadido las ubicaciones de los copos de nieve, podemos acceder a ellos como en una lista normal. El código siguiente imprimirá las coordenadas x, y de la primera ubicación guardadas en la posición cero:

print(lista_nieve[0])

¿Pero qué sucede si solo queremos una de las dos coordenadas? Pues tenemos listas dentro de listas. La lista principal contiene todas las coordenadas. Dentro de ella, cada coordenada es a su vez, una lista de una x (posición 0) y de una y (posición 1):

[[34, 10],
 [10, 50],
 [20, 18]]

Para imprimir la coordenada y en la posición 0, seleccionamos primero la coordenada 0, y luego, el valor de y en la posición 1. El código es el siguiente:

print(lista_nieve[0][1])

Para imprimir el valor de x correspondiente a la coordenada 21 (posición 20), primero seleccionamos la coordenada 20 y después el valor de x en la posición cero:

print(lista_nieve[20][0])

En el interior del bucle principal while, podemos usar un bucle for que dibuje cada elemento de la lista nieve. Recuerda, len(lista_nieve) devolverá el número de elementos en la lista de copos de nieve.

# Procesa cada copo de nieve en la lista
for i in range(len(lista_nieve)):
	# Dibuja el copo de nieve
	pygame.draw.circle(pantalla, BLANCO, lista_nieve[i], 2)

Recuerda que hay dos tipos de bucles for. Podríamos usar el otro tipo, lo que se vería así:

# Procesa UNA COPIA de la ubicación de cada copo en la lista
for xy_coord in lista_nieve:
	# Dibuja el copo de nieve
	pygame.draw.circle(pantalla, BLANCO, xy_coord, 2)

Sin embargo, como lo que planeamos es modificar las ubicaciones de los copos de nieve, no podemos usar éste ultimo tipo de bucle for, ya que estaríamos modificando la ubicación de una copia, en lugar de hacerlo con las ubicaciones reales de los copos.

Si el programa lo que quiere es que todos los objetos del array se muevan hacia abajo, tal como lo hace la nieve, expandir el bucle for creado arriba, provocará que aumente el valor de la coordenada y:

# Procesa cada copo en la lista
for i in range(len(lista_nieve)):

	# Dibuja el copo de nieve
	pygame.draw.circle(pantalla, BLANCO, lista_nieve[i], 2)
	
	# Mueve el copo un píxel hacia abajo 
	lista_nieve[i][1] += 1

Esto hace que la nieve caiga, pero una vez que llega al final de la pantalla, nada nuevo aparece. Añadiendo el código siguiente, volveremos a colocar nieve aleatoriamente en la parte superior de la pantalla:

	# Si el copo ha salido del fondo de la pantalla
	if lista_nieve[i][1] > 400:
		# Lo vuelve a situar nuevamente en la parte superior
		y = random.randrange(-50, -10)
		lista_nieve[i][1] = y
		# Le damos una nueva posición x
		x = random.randrange(0, 400)
		lista_nieve[i][0] = x

También es posible añadir otras cosas a la lista que tengan tamaños, formas, colores, velocidades y rumbos distintos. Sin embargo, esto se complica debido a los múltiples tipos de datos que necesitan ser mantenidos en la lista. De momento, lo mantendremos así de simple, pero una vez que aprendamos en el Capítulo 13 sobre “clases”, será mucho más fácil manejar los diferentes atributos de muchos objetos.

8.2.2 Listado Completo del Programa

”'
 Animating multiple objects using a list.
 Sample Python/Pygame Programs
 Simpson College Computer Science
 http://programarcadegames.com/
 http://simpson.edu/computer-science/

 Vídeo explicativo: http://youtu.be/Gkhz3FuhGoI
”'
# Importamos las bibliotecas llamadas 'pygame' y 'random'.
import pygame
import random

# Inicializamos el motor de juegos.
pygame.init()

NEGRO = [0, 0, 0]
BLANCO = [255, 255, 255]

# Establecemos el largo y ancho de la pantalla.
dimensiones = [400, 400]

pantalla = pygame.display.set_mode(dimensiones)
pygame.display.set_caption("Está Nevando")

# Creamos un array vacío
lista_nieve = []

# Iteramos 50 veces y añadimos un copo de nieve en una ubicación (x,y) aleatoria.
for i in range(50):
    x = random.randrange(0, 400)
    y = random.randrange(0, 400)
    lista_nieve.append([x, y])

reloj = pygame.time.Clock()

# Iteramos hasta que el usuario haga click sobre le botón de salida.
hecho = False
while not hecho:
    
    for evento in pygame.event.get():  # El usuario realizó alguna acción.
        if evento.type == pygame.QUIT: # Si el usuario hizo click sobre salir.
            hecho = True # Marcamos que hemos acabado y abandonamos este bucle.

    # Establecemos el color de fondo.
    pantalla.fill(NEGRO)

    # Procesamos cada copo de la lista.
    for i in range(len(lista_nieve)):
   
        # Dibujamos el copo de nieve
        pygame.draw.circle(pantalla, BLANCO, lista_nieve[i], 2)
        
        # Desplazamos un píxel hacia abajo el copo de nieve.
        lista_nieve[i][1] += 1
        
        # Si el copo se escapa del fondo de la pantalla.
        if lista_nieve[i][1] > 400:
            # Lo movemos justo encima del todo
            y = random.randrange(-50, -10)
            lista_nieve[i][1] = y
            # Le damos una nueva ubicación x
            x = random.randrange(0, 400)
            lista_nieve[i][0] = x
            
    # Avanzamos y actualizamos con lo que hemos dibujado.
    pygame.display.flip()
    reloj.tick(60)
            
# Pórtate bien con el IDLE. Si nos olvidamos de esta línea, el programa se 'colgará'
# en la salida.
pygame.quit ()


En este ejemplo hemos mostrado a cada copo de nieve moviéndose en la misma dirección. ¿Y si lo que necesitáramos fuera que cada objeto se moviera por separado? Cada uno su propio rumbo. Si es eso lo que necesitas, mírate el Capítulo 13 donde tratamos el uso de clases. En el Taller 8 te guiamos para conseguir cientos de objetos animados diferentes, cada uno siguiendo su propio rumbo.

8.3 Animación 3D

Vídeo: Demo del Motor de Juegos Blender

Extenderse desde un entorno 2D a otro completamente 3D, usando física de juegos, no es tan difícil como parece a primera vista. Aunque ello se encuentre fuera del ámbito de esta clase, vendría bien echarle un vistazo a la forma de conseguirlo.

Existe un programa 3D gratuito llamado Blender, el cual posee un “motor de juegos” que permite a los programadores la creación de juegos 3D. Los objetos 3D en el juego pueden llevar junto a ellos, código Python para controlar sus acciones.

fig.blender01
Figure 8.3: Ejemplo de Archivo Blender

Observa la Figura 8.3. Nos muestra una bandeja verde con varios objetos en su interior. El objeto azul está controlado por un script de Python que lo mueve por la bandeja, haciendo que rebote contra los otros objetos. El guión tiene muchos de los rasgos que tienen otros programas 2D. Hay un bucle principal, una lista de coordenadas x, y, así como variables que controlan el vector.

El bucle principal del programa es controlado por Blender. El código Python mostrado, es llamado por Blender en cada “fotograma” que el juego reproduce. Es por esto que no vemos en el código de Python un bucle principal. De todas formas existe.

El objeto azul posee una ubicación en formato x, y, z. Podemos acceder a él y modificarlo usando la variable blueobject.position. La posición 0 del array contiene la ubicación de x, la posición 1 la de y, y la posición 2 la de z.

En lugar de las variables change_x y change_y empleadas en los ejemplos 2D, en el caso del ejemplo con Blender se usa el array asociativo de ubicaciones:
blue_object["x_change"]
blue_object["y_change"]

La instrucción if comprueba si el objeto azul ha alcanzado los bordes de la pantalla y que el rumbo debe revertirse. En lugar de emplear píxeles como en los juegos 2D, las ubicaciones de los objetos pueden ser números del tipo real (float point). Ubicar un elemento entre 5 y 6, por ejemplo en 5.5, es totalmente legal.

# Importamos El Motor de Juegos Blender
import bge

# Obtenemos una referencia para el objeto azul
cont = bge.logic.getCurrentController()
blue_object = cont.owner

# Imprimimos las coordenadas x,y donde se encuentra el objeto azul
print(blue_object.position[0], blue_object.position[1] )

# Cambiamos las coordenadas x,y en función de x_change e
# y_change. x_change e y_change son propiedades del juego
# asociadas al objeto azul.
blue_object.position[0] += blue_object["x_change"]
blue_object.position[1] += blue_object["y_change"]

# Comprobar si el objeto ha alcanzado un lado.
# Si es así, revertir su rumbo. Hacerlo para los cuatro lados.
if blue_object.position[0] > 6 and blue_object["x_change"] > 0:
    blue_object["x_change"] *= -1

if blue_object.position[0] < -6 and blue_object["x_change"] < 0:
    blue_object["x_change"] *= -1

if blue_object.position[1] > 6 and blue_object["y_change"] > 0:
    blue_object["y_change"] *= -1

if blue_object.position[1] < -6 and blue_object["y_change"] < 0:
    blue_object["y_change"] *= -1

Podemos descargar Blender desde:
http://www.blender.org/

Un ejemplo completo con Blender está disponible en:
ProgramArcadeGames.com/chapters/08_intro_to_animation/simple_block_move.blend.

8.4 Repaso

8.4.1 Test

Haz click para ir al Test.

8.4.2 Ejercicios

Haz click para ir a los Ejercicios.

8.4.3 Taller

Haz click para ir al Taller.


You are not logged in. Log in here and track your progress.