Construir Actividades con Pygame
Introducción
PyGame y PyGTK son dos maneras distintas de crear un programa en Python con una interfaz gráfica. En general no se usan ambas en un mismo programa. Cada una de ellas tiene su propia forma de crear una ventana y gestionar los eventos.
La clase básica Activity que hemos venido usando es una extensión de la clase Ventana PyGTK y emplea la gestión de eventos de PyGTK. Las barras de herramientas empleadas por todas las Actividades son componentes PyGTK. En resumen, cualquier Actividad creada en Python debe usar PyGTK. Insertar un programa PyGame en el medio de un programa PyGTK se asemeja a colocar un modelo de barco dentro de una botella. Afortunadamente existe un código Python llamado SugarGame que lo hace posible.
Antes de ver cómo lo colocamos dentro de la botella, veamos un poco más de cerca nuestro barco modelo.
Crear un programa con PyGame
Como cabría esperar, es una buena idea construir un juego en Python usando Pygame antes de crear una Actividad con él. No soy un experimentado desarrollador en Pygame, pero usando el manual Rapid Game Development with Python de Richard Jones en esta URL:
http://richard.cgpublisher.com/product/pub.84/prod.11
Me fue posible crear un juego modesto en apenas un día. Pudo haber sido antes, pero los ejemplos del manual contenían errores y requerí bastante tiempo usando The GIMP para crear imágenes que pudieran servir de sprites en el juego.
Los Sprites son pequeñas imágenes, a menudo animadas, que representan objetos en un juego. En general tienen un fondo transparente que les permite dibujarse sobre una imagen de fondo. Usé el formato PNG ya que él me permite usar un alpha channel (otro término para decir que una parte de la imagen es transparente).
PyGame dispone de código para desplegar imágenes de fondo, para crear sprites y moverlos sobre el fondo elegido y para detectar cuándo los sprites chocan entre sí y hacer algo cuando esto sucede. Esta es la base para construir juegos en 2D. Existe una gran cantidad de juegos escritos con PyGame que pueden convertirse fácilmente en actividades de Sugar.
Mi juego se asemeja mucho al juego del autito en el manual, pero en lugar de un auto dibujé un avión. Este avión es el Demoiselle creado por Alberto Santos-Dumont en 1909. Encontrarán también cuatro estudiantes de Otto Lilienthal que se sostienen en el aire gracias a sus "alas delta" (planeadores). Las alas delta caen cuando Santos-Dumont choca contra ellas. Los controles empleados en el juego fueron también modificados. Usé las teclas "+" y "-" tanto en el teclado principal como el secundario, más las teclas "9" y "3", para abrir y cerrar el acelerador así como las teclas hacia arriba y hacia abajo en ambos teclados para mover el joystick hacia adelante y hacia atrás. Usar el teclado secundario es útil por un par de razones. Primero algunas versiones del sugar-emulator no reconocen las flechas del teclado principal. Segundo, las flechas del teclado se corresponden con el controlador de juego de la laptop XO y las teclas que no son flechas se corresponden con los otros botones en la pantalla de la XO. Estos botones pueden ser usados para jugar al juego cuando la XO está en modo tablet.
Como simulador de vuelo no es gran cosa, pero demuestra al menos algunas de las cosas que PyGame puede hacer. A continuación les dejo el código del juego, al que llamé Demoiselle:
#! /usr/bin/env python
import pygame
import math
import sys
class Demoiselle:
"This is a simple demonstration of using PyGame \
sprites and collision detection."
def __init__(self):
self.background = pygame.image.load('sky.jpg')
self.screen = pygame.display.get_surface()
self.screen.blit(self.background, (0, 0))
self.clock = pygame.time.Clock()
self.running = True
gliders = [
GliderSprite((200, 200)),
GliderSprite((800, 200)),
GliderSprite((200, 600)),
GliderSprite((800, 600)),
]
self. glider_group = pygame.sprite.RenderPlain(
gliders)
def run(self):
"This method processes PyGame messages"
rect = self.screen.get_rect()
airplane = AirplaneSprite('demoiselle.png',
rect.center)
airplane_sprite = pygame.sprite.RenderPlain(
airplane)
while self.running:
self.clock.tick(30)
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
return
elif event.type == pygame.VIDEORESIZE:
pygame.display.set_mode(event.size,
pygame.RESIZABLE)
self.screen.blit(self.background,
(0, 0))
if not hasattr(event, 'key'):
continue
down = event.type == pygame.KEYDOWN
if event.key == pygame.K_DOWN or \
event.key == pygame.K_KP2:
airplane.joystick_back = down * 5
elif event.key == pygame.K_UP or \
event.key == pygame.K_KP8:
airplane.joystick_forward = down * -5
elif event.key == pygame.K_EQUALS or \
event.key == pygame.K_KP_PLUS or \
event.key == pygame.K_KP9:
airplane.throttle_up = down * 2
elif event.key == pygame.K_MINUS or \
event.key == pygame.K_KP_MINUS or \
event.key == pygame.K_KP3:
airplane.throttle_down = down * -2
self.glider_group.clear(self.screen,
self.background)
airplane_sprite.clear(self.screen,
self.background)
collisions = pygame.sprite.spritecollide(
airplane,
self.glider_group, False)
self.glider_group.update(collisions)
self.glider_group.draw(self.screen)
airplane_sprite.update()
airplane_sprite.draw(self.screen)
pygame.display.flip()
class AirplaneSprite(pygame.sprite.Sprite):
"This class represents an airplane, the Demoiselle \
created by Alberto Santos-Dumont"
MAX_FORWARD_SPEED = 10
MIN_FORWARD_SPEED = 1
ACCELERATION = 2
TURN_SPEED = 5
def __init__(self, image, position):
pygame.sprite.Sprite.__init__(self)
self.src_image = pygame.image.load(image)
self.rect = pygame.Rect(
self.src_image.get_rect())
self.position = position
self.rect.center = self.position
self.speed = 1
self.direction = 0
self.joystick_back = self.joystick_forward = \
self.throttle_down = self.throttle_up = 0
def update(self):
"This method redraws the airplane in response\
to events."
self.speed += (self.throttle_up +
self.throttle_down)
if self.speed > self.MAX_FORWARD_SPEED:
self.speed = self.MAX_FORWARD_SPEED
if self.speed < self.MIN_FORWARD_SPEED:
self.speed = self.MIN_FORWARD_SPEED
self.direction += (self.joystick_forward + \
self.joystick_back)
x_coord, y_coord = self.position
rad = self.direction * math.pi / 180
x_coord += -self.speed * math.cos(rad)
y_coord += -self.speed * math.sin(rad)
screen = pygame.display.get_surface()
if y_coord < 0:
y_coord = screen.get_height()
if x_coord < 0:
x_coord = screen.get_width()
if x_coord > screen.get_width():
x_coord = 0
if y_coord > screen.get_height():
y_coord = 0
self.position = (x_coord, y_coord)
self.image = pygame.transform.rotate(
self.src_image, -self.direction)
self.rect = self.image.get_rect()
self.rect.center = self.position
class GliderSprite(pygame.sprite.Sprite):
"This class represents an individual hang \
glider as developed by Otto Lilienthal."
def __init__(self, position):
pygame.sprite.Sprite.__init__(self)
self.normal = pygame.image.load(
'glider_normal.png')
self.rect = pygame.Rect(self.normal.get_rect())
self.rect.center = position
self.image = self.normal
self.hit = pygame.image.load('glider_hit.png')
def update(self, hit_list):
"This method redraws the glider when it collides\
with the airplane and when it is no longer \
colliding with the airplane."
if self in hit_list:
self.image = self.hit
else:
self.image = self.normal
def main():
"This function is called when the game is run \
from the command line"
pygame.init()
pygame.display.set_mode((0, 0), pygame.RESIZABLE)
game = Demoiselle()
game.run()
sys.exit(0)
if __name__ == '__main__':
main()
Y aquí tenemos al juego en acción:

Encontrarán el código de este juego en el archivo demoiselle.py que se encuentra en el libro de ejemplos del proyecto en Git.
Introducción a SugarGame
SugarGame no es una parte de Sugar propiamente dicha. Si deseas usarla deberás incluir el código Python para SugarGame dentro del bundle de tu Actividad. Incluí la versión de SugarGame que estoy usando en el proyecto del libro de ejemplos en el directorio sugargame, pero cuando hagas tus propios juegos deberías asegurarte de que dispones de la última versión. Puedes hacer esto bajando el proyecto desde Gitorious empleando estos comandos:
mkdir sugargame
cd sugargame
git clone git://git.sugarlabs.org/sugargame/mainline.git
Verás dos subdirectorios en este proyecto: sugargame y test, más un archivo README.txt que contiene información para usar sugargame en tus propias Actividades. El subdirectorio test contiene un sencillo programa PyGame que puede ser ejecutado solo (TestGame.py) o como una Actividad (TestActivity.py).
Si ejecutas el TestGame.py desde la línea de comandos verás una pelota que rebota en un fondo blanco. Para ejecutar la versión Actividad debes escribir:
./setup.py dev
desde la línea de comandos. No pude hacer funcionar la Actividad bajo el emulador de Sugar hasta que realicé los dos siguientes cambios:
- Hice una copia del directorio sugargame dentro del directorio test.
- Borré la línea que contenía "sys.path.append(..) # Import sugargame package from top directory." en el TestActivity.py. Obviamente, esta línea debería ayudar al programa a encontrar el directorio sugargame en el proyecto, pero no funcionó bajo Fedora 10. Tu experiencia puede ser distinta.
La Actividad se ve así:

La barra de herramientas de PyGame tiene un solo botón que te permite hacer rebotar o detener la pelota.
Crear una Actividad Sugar a partir de un programa PyGame.
Llegó el momento de poner nuestro modelo de barco en la botella. Lo primero es hacer una copia del directorio sugargame del proyecto SugarGame en el directorio raíz de nuestro propio proyecto.
Vale la pena leer el archivo README.txt del proyecto SugarGame. Nos explica cómo crear una Actividad basada en el ejemplo TestActivity.py en el proyecto Sugargame.Ésta será nuestra botella. El siguiente es el código para la mía, que llamé DemoiselleActivity.py:
# DemoiselleActivity.py
from gettext import gettext as _
import gtk
import pygame
from sugar.activity import activity
from sugar.graphics.toolbutton import ToolButton
import gobject
import sugargame.canvas
import demoiselle2
class DemoiselleActivity(activity.Activity):
def __init__(self, handle):
super(DemoiselleActivity, self).__init__(handle)
# Build the activity toolbar.
self.build_toolbar()
# Create the game instance.
self.game = demoiselle2.Demoiselle()
# Build the Pygame canvas.
self._pygamecanvas = \
sugargame.canvas.PygameCanvas(self)
# Note that set_canvas implicitly calls
# read_file when resuming from the Journal.
self.set_canvas(self._pygamecanvas)
self.score = ''
# Start the game running.
self._pygamecanvas.run_pygame(self.game.run)
def build_toolbar(self):
toolbox = activity.ActivityToolbox(self)
activity_toolbar = toolbox.get_activity_toolbar()
activity_toolbar.keep.props.visible = False
activity_toolbar.share.props.visible = False
self.view_toolbar = ViewToolbar()
toolbox.add_toolbar(_('View'), self.view_toolbar)
self.view_toolbar.connect('go-fullscreen',
self.view_toolbar_go_fullscreen_cb)
self.view_toolbar.show()
toolbox.show()
self.set_toolbox(toolbox)
def view_toolbar_go_fullscreen_cb(self, view_toolbar):
self.fullscreen()
def read_file(self, file_path):
score_file = open(file_path, "r")
while score_file:
self.score = score_file.readline()
self.game.set_score(int(self.score))
score_file.close()
def write_file(self, file_path):
score = self.game.get_score()
f = open(file_path, 'wb')
try:
f.write(str(score))
finally:
f.close
class ViewToolbar(gtk.Toolbar):
__gtype_name__ = 'ViewToolbar'
__gsignals__ = {
'needs-update-size': (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
([])),
'go-fullscreen': (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
([]))
}
def __init__(self):
gtk.Toolbar.__init__(self)
self.fullscreen = ToolButton('view-fullscreen')
self.fullscreen.set_tooltip(_('Fullscreen'))
self.fullscreen.connect('clicked',
self.fullscreen_cb)
self.insert(self.fullscreen, -1)
self.fullscreen.show()
def fullscreen_cb(self, button):
self.emit('go-fullscreen')
Es un poco más "bonita" que TestActivity.py. Decidí que mi juego no necesitaba ser pausado y continuado, así que remplacé la barra de herramientas de PyGame con un una barra de herramientas View que le permite al usuario ocultar la barra cuando no es necesaria. Usé los métodos read_file() y write_file() para guardar y recuperar el score del juego. También oculté los controles Keep and Share en la barra principal.
Tal como podrías esperar, poner un barco dentro de una botella requiere modificar el barco. Ésta es la nueva versión, demoiselle2.py, que incluye las siguientes modificaciones:
#! /usr/bin/env python
import pygame
import gtk
import math
import sys
class Demoiselle:
"This is a simple demonstration of using PyGame \
sprites and collision detection."
def __init__(self):
self.clock = pygame.time.Clock()
self.running = True
self.background = pygame.image.load('sky.jpg')
def get_score(self):
return '99'
def run(self):
"This method processes PyGame messages"
screen = pygame.display.get_surface()
screen.blit(self.background, (0, 0))
gliders = [
GliderSprite((200, 200)),
GliderSprite((800, 200)),
GliderSprite((200, 600)),
GliderSprite((800, 600)),
]
glider_group = pygame.sprite.RenderPlain(gliders)
rect = screen.get_rect()
airplane = AirplaneSprite('demoiselle.png',
rect.center)
airplane_sprite = pygame.sprite.RenderPlain(
airplane)
while self.running:
self.clock.tick(30)
# Pump GTK messages.
while gtk.events_pending():
gtk.main_iteration()
# Pump PyGame messages.
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
return
elif event.type == pygame.VIDEORESIZE:
pygame.display.set_mode(event.size,
pygame.RESIZABLE)
screen.blit(self.background, (0, 0))
if not hasattr(event, 'key'):
continue
down = event.type == pygame.KEYDOWN
if event.key == pygame.K_DOWN or \
event.key == pygame.K_KP2:
airplane.joystick_back = down * 5
elif event.key == pygame.K_UP or \
event.key == pygame.K_KP8:
airplane.joystick_forward = down * -5
elif event.key == pygame.K_EQUALS or \
event.key == pygame.K_KP_PLUS or \
event.key == pygame.K_KP9:
airplane.throttle_up = down * 2
elif event.key == pygame.K_MINUS or \
event.key == pygame.K_KP_MINUS or \
event.key == pygame.K_KP3:
airplane.throttle_down = down * -2
glider_group.clear(screen, self.background)
airplane_sprite.clear(screen, self.background)
collisions = pygame.sprite.spritecollide(
airplane,
glider_group, False)
glider_group.update(collisions)
glider_group.draw(screen)
airplane_sprite.update()
airplane_sprite.draw(screen)
pygame.display.flip()
class AirplaneSprite(pygame.sprite.Sprite):
"This class represents an airplane, the Demoiselle \
created by Alberto Santos-Dumont"
MAX_FORWARD_SPEED = 10
MIN_FORWARD_SPEED = 1
ACCELERATION = 2
TURN_SPEED = 5
def __init__(self, image, position):
pygame.sprite.Sprite.__init__(self)
self.src_image = pygame.image.load(image)
self.rect = pygame.Rect(self.src_image.get_rect())
self.position = position
self.rect.center = self.position
self.speed = 1
self.direction = 0
self.joystick_back = self.joystick_forward = \
self.throttle_down = self.throttle_up = 0
def update(self):
"This method redraws the airplane in response\
to events."
self.speed += (self.throttle_up +
self.throttle_down)
if self.speed > self.MAX_FORWARD_SPEED:
self.speed = self.MAX_FORWARD_SPEED
if self.speed < self.MIN_FORWARD_SPEED:
self.speed = self.MIN_FORWARD_SPEED
self.direction += (self.joystick_forward +
self.joystick_back)
x_coord, y_coord = self.position
rad = self.direction * math.pi / 180
x_coord += -self.speed * math.cos(rad)
y_coord += -self.speed * math.sin(rad)
screen = pygame.display.get_surface()
if y_coord < 0:
y_coord = screen.get_height()
if x_coord < 0:
x_coord = screen.get_width()
if x_coord > screen.get_width():
x_coord = 0
if y_coord > screen.get_height():
y_coord = 0
self.position = (x_coord, y_coord)
self.image = pygame.transform.rotate(
self.src_image, -self.direction)
self.rect = self.image.get_rect()
self.rect.center = self.position
class GliderSprite(pygame.sprite.Sprite):
"This class represents an individual hang \
glider as developed by Otto Lilienthal."
def __init__(self, position):
pygame.sprite.Sprite.__init__(self)
self.normal = pygame.image.load(
'glider_normal.png')
self.rect = pygame.Rect(self.normal.get_rect())
self.rect.center = position
self.image = self.normal
self.hit = pygame.image.load('glider_hit.png')
def update(self, hit_list):
"This method redraws the glider when it collides\
with the airplane and when it is no longer \
colliding with the airplane."
if self in hit_list:
self.image = self.hit
else:
self.image = self.normal
def main():
"This function is called when the game is run \
from the command line"
pygame.init()
pygame.display.set_mode((0, 0), pygame.RESIZABLE)
game = Demoiselle()
game.run()
sys.exit(0)
if __name__ == '__main__':
main()
¿Por qué no cargar ambas versiones demoiselle.py y demoiselle2.py en Eric y tomar unos pocos minutos para ver si puedes encontrar que cambió entre ellas? Sorprendentemente, las diferencias son muy pocas. Agregué algo de código al bucle principal de PyGame para verificar eventos de PyGTK y lidiar con ellos:
while self.running:
self.clock.tick(30)
# Pump GTK messages.
while gtk.events_pending():
gtk.main_iteration()
# Pump PyGame messages.
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
return
elif event.type == pygame.VIDEORESIZE:
pygame.display.set_mode(event.size,
pygame.RESIZABLE)
screen.blit(self.background, (0, 0))
if not hasattr(event, 'key'):
continue
down = event.type == pygame.KEYDOWN
if event.key == pygame.K_DOWN or \
... continue dealing with PyGame events ...
Esto permite que PyGame y PyGTK se alternen gestionando eventos. Si este código no estuviera presente, los eventos de GTK serían ignorados y sería imposible cerrar la Actividad, ocultar la barra de herramientas, etc. Necesitamos agregar import gtk al comienzo del archivo para que estos métodos puedan ser encontrados.
Evidentemente, también agregué los métodos para fijar y devolver scores:
def get_score(self):
return self.score
def set_score(self, score):
self.score = score
El mayor cambio está en el método __init__() de la clase Demoiselle. Al principio tenía código para mostrar la imagen del fondo en la pantalla:
def __init__(self):
self.background = pygame.image.load('sky.jpg')
self.screen = pygame.display.get_surface()
self.screen.blit(self.background, (0, 0))
El problema con esto es que sugargame creará un objeto PyGTK de pantalla especial para remplazar la pantalla de PyGame antes que lo haga el código de la Actividad Demoiselle, por lo que self.screen tendrá valor "nulo". La única forma de superar este problema es mover cualquier código que se refiera al display out del método __init__() de la clase y al principio del método que contiene el bucle del evento. Esto te puede dejar con un método __init__() que hace poco o nada. Lo único que puedes querer es código para crear variables de instancias.
Nada de lo que hemos hecho a demoiselle2.py inhibe la posibilidad de ejecutarlo como un programa Python independiente.
Para probar el juego ejecuta ./setup.py dev desde el directorio Making_Activities_Using_PyGame. Cuando pruebes la Actividad se debería ver así:

- Traducido Fernando Cormenzana, Uruguay^





