Razón Artificial

La ciencia y el arte de crear videojuegos

Gestionando Escenas con Pygame

Escrito por adrigm el 25 de agosto de 2010 en Desarrollo Videojuegos, Programación | 30 Comentarios.

Un videojuego generalmente no se compone de una sola pantalla, sino que hay varias como pueden ser un menú introductorio. el mapa de nuestro juego, un menú de objetos, una pantalla de puntuaciones, etc. Estas diferentes pantallas reciben el nombre de escenas cada una de ella representa algo especifico de nuestro juego.

Cambiar la escena del juego puede ser algo complicado, recuerda que siempre se debe mantener el bucle de ejecución de los juegos.

Se suele utilizar un bucle infinito que vaya manteniendo esto, pero surge el problema de que en una escena determinada de nuestro juego los eventos, la lógica y las cosas a dibujar serán unas y en otras escenas puede ser otra. Por ejemplo, en un menú habrá que gestionar como eventos si se  acciona algún botón y habrá que dibujar este menú, en otra escena del juego puedes necesitar dibujar otra cosa.

En la imagen superior podemos ver dos escenas diferentes del juego Age of Empires III a la izquierda el menú principal y a la derecha un mapa del juego.

El objeto director y la escena maestra

Nuestras escenas a pesar de lo diferente que puedan ser entre ellas siempre van a tener las características del bucle de arriba. Es decir, deben de inicializarse, actualizarse, gestionar eventos y dibujarse. Por tanto podríamos definir una clase maestra abstracta que se encargara de hacer esto con la escena que tengamos activa independiente de cual sea.

EL objeto director

En lugar de gestionar en nuestro archivo main el bucle del juego lo que vamos a hacer es crear un clase director que será la encargada de gestionar nuestro bucle de juego, este bucle recibirá una escena cualquiera y se encargará de ejecutar sus métodos de actualizar, eventos y dibujar.

# -*- encoding: utf-8 -*-

# Módulos
import pygame
import sys

class Director:
    """Representa el objeto principal del juego.

    El objeto Director mantiene en funcionamiento el juego, se
    encarga de actualizar, dibuja y propagar eventos.

    Tiene que utilizar este objeto en conjunto con objetos
    derivados de Scene."""

    def __init__(self):
        self.screen = pygame.display.set_mode((640, 480))
        pygame.display.set_caption("Nombre Proyecto")
        self.scene = None
        self.quit_flag = False
        self.clock = pygame.time.Clock()

    def loop(self):
        "Pone en funcionamiento el juego."

        while not self.quit_flag:
            time = self.clock.tick(60)
            
            # Eventos de Salida
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.quit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
			self.quit()

            # detecta eventos
            self.scene.on_event()

            # actualiza la escena
            self.scene.on_update()

            # dibuja la pantalla
            self.scene.on_draw(self.screen)
            pygame.display.flip()

    def change_scene(self, scene):
        "Altera la escena actual."
        self.scene = scene

    def quit(self):
        self.quit_flag = True

Este objeto debe crearse dentro de nuestra función principal main en sustitución del bucle del juego. Vamos a explicarlo:

En el constructor creamos la ventana de Pyagame, le damos título e iniciamos el reloj. También definimos varias variables, por un lado tenemos self.scene = None que será la escena que queremos que represente, de momento ninguna. self.quit_flag = False Es una “bandera” que nos indica si queremos salir.

Luego viene el método loop que es el que contiene el bucle de nuestro juego, tiene como condición para salir del bucle que la variable self.quit_flag sea verdadera, cuando lo sea saldrá del bucle. para hacer esta variable verdadera solo hay que llamar desde cualquier lado del bucle al método quit que como vemos cambia la variable a verdadera.

Ya dentro del bucle lo primero es establecer el framerate, en este caso lo tenemos a 60. Luego ya empezamos con el bucle, comprobamos si se ha producido un evento de salida. En nuestro caso aparte de eso comprobamos también como evento de salida si se ha pulsado la tecla escape (esto a gusto del consumidor y del juego). Si se ha producido un evento de salida como vemos llamamos a self.quit().

Luego llama a self.scene.on_event(), pero, ¿self.scene no valía None? Exacto así que hacemos un inciso aquí en la clase director para explicar la clase scene.

La clase Scene

La clase Scene representa a una clase abstracta del juego. Es una clase padre que debe ser heredada por todas las escenas de nuestro juego, veamos.

class Scene:
    """Representa un escena abstracta del videojuego.

    Una escena es una parte visible del juego, como una pantalla
    de presentación o menú de opciones. Tiene que crear un objeto
    derivado de esta clase para crear una escena utilizable."""

    def __init__(self, director):
        self.director = director

    def on_update(self):
        "Actualización lógica que se llama automáticamente desde el director."
        raise NotImplemented("Tiene que implementar el método on_update.")

    def on_event(self, event):
        "Se llama cuando llega un evento especifico al bucle."
        raise NotImplemented("Tiene que implementar el método on_event.")

    def on_draw(self, screen):
        "Se llama cuando se quiere dibujar la pantalla."
        raise NotImplemented("Tiene que implementar el método on_draw.")

Como vemos es muy sencilla, el constructor solo recibe como parámetro el objeto director. Luego implementa tres métodos. on_update, on_event y on_draw. Cada uno se encarga de hacer las tareas de actualizar, gestionar eventos y dibujar respectivamente. Pero como vemos en el cuerpo de cada uno de los métodos lo que hay es un error de que no de ha implementado ese método, esto es porque Scene es una escena padre que debe ser llamada por las escenas de nuestro juego e implementar estos tres métodos para que hagan las acciones específicas de nuestra escena. Vamos allá.

Creando una escena

Vamos a crear una sencilla escena que no haga nada, simplemente que exista:

class SceneHome(scene.Scene):
    """Escena inicial del juego, esta es la primera que se carga cuando inicia"""
    
    def __init__(self, director):
        scene.Scene.__init__(self, director)
       
    def on_update(self):
        pass

    def on_event(self):
        pass

    def on_draw(self, screen):
        pass

Como vemos nuestra escena hereda de Scene (puedes ver que en el código pone scene.Scene, esto es porque yo la tengo en un archivo aparte llamada scene), como vemos en el __init__ lo primero que hacemos es llamar al __init__ de Scene y pasarle el objeto director. Luego implementamos y sustituimos los métodos de la clase padre con los de nuestras escena. En esta caso los tres con la sentencia pass, que no hace nada.

Veamos como inicalizar la escena, volviendo al archivo main.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Módulos
import pygame
import director
import scene_home

def main():
    dir = director.Director()
    scene = scene_home.SceneHome(dir)
    dir.change_scene(scene)
    dir.loop()

if __name__ == '__main__':
    pygame.init()
    main()

Este es el archivo main de nuestro proyecto, como vemos importa tres módulos, pygame para que pueda ser inicializado, el módulo director que contiene la clase director y el modulo scene_home que contiene la escena con la que queremos iniciar nuestro juego.

Luego ya en la función main creamos el objeto director con dir = director.Director(). Esto llama al constructor del objeto director que como vimos arriba crea la ventana, le pone titulo, crea el reloj y define la variable self.scene = None.

A continuación creamos un objeto scene con nuestra escena: scene = scene_home.SceneHome(dir). y llamamos al método change_scene de la clase director pasándole como parámetro el objeto scene que acabamos de crear, veamos que hace este método.

def change_scene(self, scene):
    "Altera la escena actual."
    self.scene = scene

Pues como era de esperar establece que la variable self.scene del objeto director sea el objeto scene que acabamos de crear. a continuación llama a dir.loop() que es el bucle de nuestro juego que dejamos a medias. Volvamos a él.

Continuando con el objeto director

Nos quedaba por explicar lo siguiente del objeto director.

# detecta eventos
self.scene.on_event()

# actualiza la escena
self.scene.on_update()

# dibuja la pantalla
self.scene.on_draw(self.screen)
pygame.display.flip()

Pues lo que hace es llamar a los tres métodos de nuestra escena cargada que se encarga de gestionar las actualizaciones, los eventos y el dibujado. Por último se llama a pygame.display.flip()
para dibujar la pantalla.

Como vemos ya tenemos la manera de cargar una escena cualquiera desde nuestro objeto director con sus propios métodos de actualización, eventos y dibujado. Ahora para cambiar de scene solo hace falta llamar desde cualquiera parte dentro de nuestra escena activa a director.change_scene(scene) donde scene es la nueva scena que queremos que pase a tener el control ya que esto modificará a self.scene que es la que implementa los métodos.

Puede resultar lioso así de primera, pero a continuación dejo el código de este ejemplo. Si no lo entiendes a la primera el artículo mira el código y trata de probar cosas que puede ser lioso si solo se lee de pasada.

30 Comentarios en "Gestionando Escenas con Pygame"

  1. Manuel dice:

    Una vez estuve haciendo cosas muy similares en C++, lo que defines como clase Director es completamente correcta, pero yo tenía dos aspectos más:

    1.- La clase Director la definí mediante un patrón ‘Singleton’, si no sabes nada de patrones software echa un vistazo en la Wikipedia, te resultará muy útil. Este patrón viene a definir lo siguiente: Sólo puede existir una clase Director, en caso de que alguien cree o invoque una clase Director dentro de la misma aplicación, siempre se devuelve el mismo objeto, lo cual es útil para el siguiente punto:

    2.- Se dispone de una pila de ‘escenas’. El Director no conoce el orden de las escenas, puesto que estas pueden variar dependiendo de los eventos del juego (pausa, cambio de nivel, nivel secreto…), son las propias escenas las que invocan una instancia del objeto Director y apilan la siguiente escena.

    Espero que te sirva de ayuda.

    Estás haciendo un gran trabajo con el blog, ¡ánimo!

  2. adrigm dice:

    En cuanto a lo primero, sería bueno que siempre se devolviera el mismo objeto director, pero supongo que eso ya queda a disposición del que lo implemente en su juego y con que propósito. Por ejemplo, en un Engine sería necesario pues sobre él se construirían juegos sin saber como está programado, pero a lo mejor para proyectos más básicos en el que uno solo es el programador él ya sabe que no debe llamar más de una vez al objeto director. A parte de que pienso que no tendría mucho sentido. Pero como bien dices, mejor implementarlo y evitar errores.

    Respecto a lo segundo, es así como está hecho. El que cambia la escena que maneja el director es la propia escena que esta cargada, es en esta donde se selecciona la que debe ser la nueva escena.

    Gracias por los ánimos.

  3. […] el tutorial de la gestión de escenas con pygame, pues lo […]

  4. […] en el artículo anterior. Vuelvo a recomendar encarecidamente leer y entender el artículo de gestión de escenas, pues no voy a volver a […]

  5. Satanas dice:

    Hola amigo, mis felicitaciones por los excelentes tutoriales.

    Te comento que en el código del Director tienes un pequeño error: la línea 35 debe ir identada correctamente, de lo contrario la aplicación saldrá al ejecutar la primera iteración del bucle (pues llamas a self.quit() dentro del for) ;)

    De resto muy bueno el contenido,

    Saludos

  6. adrigm dice:

    Cierto, gracias Satanas. Es más el código daría error directamente al estar el cuerpo de if superior vacío.

    Edit: Pues está identado correctamente, es el plugin del código que está haciendo lo que le da la gana.

  7. Francis dice:

    Hola Adrigm quiero felicitarte por tu interes en que los otros aprendan y aprovecho para hacerte una pregunta acerca de este capitulo,tengo todo los ficheros listos para usarse pero cuando lanzo main.py la ventana no sale

  8. adrigm dice:

    Francis, ¿Te da algún error la consola o se queda el programa ejecutándose? Necesito mas datos para ayudarte. Versiones de python y pygame, SO…

  9. Federico dice:

    Hola, te vuelo a felicitar por los tutoriales la verdad muy buenos.

    Tengo una pregunta, vos decis que tenes diferentes escenas, una para el menu, otras para cuando estes jugando, aca viene mi pregunta, vos tenes una escena sola para cuando juegan?

    Por ejemplo vos tenes diferentes niveles, donde aparecen diferentes enemigos y tmb diferentes imagenes, sería una escena diferente por cada nivel?

  10. adrigm dice:

    Federico, todo depende de como lo pantees y el estilo de juego. Por ejemplo en un plataforma no tendría sentido cada nivel en una escena ya que los controles no varían ni el tratamiento de la escena. Lo que cambiaría sería el mapeado y sprites (no confundir con la escena) por lo que yo lo combinaría todo dentro de la misma escena. Los cambios de escena los dejaría para cambios importante, como un menu, un minijuego donde los controles son diferentes, etc.

    En el ejemplo del artículos puedes ver como el menu y el mapeado del juego son diferentes, pero independientemente del mapa que cargues la escena es la misma, es decir puedes realizar las mismas acciones en un mapa y otro.

    Como ya digo todo depende del juego, pero imagina que tienes 200 niveles, haces 200 escenas?

  11. yaderv dice:

    Había seguido el tutorial del juego de pong y todo excelente, hasta que me le hice un menú y quise incorporar esto.. el resultado, estoy más confundido y perdido que nunca :(

  12. adrigm dice:

    Decir que estás confundido no ayuda, paste por el foro y expón tu problema código y demás y trataré de ayudarte.

  13. yaderv dice:

    Vale que era un problema de lógica, bastó con ver los artículos de arkanoid para saber a que te refieres en este post.

    Pero aún no entiendo la razón de heredar de una clase padre Scene =/

  14. adrigm dice:

    La herencia es para darle funcionalidad, puedes definir comportamientos genéricos de las escenas, además de que te aseguras de que si no se implementa alguno de los métodos bases se llamará al método de Scene que devolverá un error o lo que tu quieras.

    En otros lenguajes se ve más claro como en C++ donde serían métodos virtuales y es necesario una clase base común.

  15. julian9512 dice:

    Disculpa tengo un prgrama hecho y me decidi a aplicarle unos menus y unirlo con otra cosas similares que tengo. Tengo unos valores globales, los cuales los guardo en un archivo config.py. El problema esta en que cuando a mi archivo scene_home(tiene otro nombre en realidad) pongo esos valores me dice que no existen como valores globales.

    PS: En la clase scene_home importo a config.py (es por eso que no entiendo cual es el problema)

  16. adrigm dice:

    Probablemente estés importando de esta forma:

    import config
    

    por lo que para usar una variable de ese módulo tendrías que hacer:

    config.mi_variable
    

    Para acceder directamente a los valores tendrías que importar así:

    from config import *
    

    Yo prefiero la primera para que no haya conflictos con nombres iguales de diferentes módulos importados y para saber de un vistazo a que módulo pertenece la variable/clase/función.

  17. julian9512 dice:

    Tienes todo la razon, me lo pase por alto. El problema es que ahora me surgio otra prgunta.
    Como ya mencione antes, yo ya tenia un programa y me decidi a pasarlo. Entonces, el problema ahora surge cuando yo quiero tomar eventos que ocurren dentro de una sola escena.

    Cuando llamo a estos eventos (Hago clik en los sprites) para que estos respondan hay que hacerle dos click y mover el mouse (algo rarisimo). En lo que tenia antes (el programa original) todo anda bien.

    Hasta ahora tengo esto en el def on_event(Perdon, pero no recuerdo la pagina para poder subir el codigo):

    for eventos in pygame.event.get():
    			if eventos.type == MOUSEBUTTONDOWN:          
    				if eventos.button == 1:                           
    					self.cubo1.object(eventos.pos, self.lista21)
                   				self.cubo2.object(eventos.pos, self.lista22)                
    					self.cubo3.object(eventos.pos, self.lista23)                
    					self.cubo4.object(eventos.pos, self.lista24)                
    					self.cubo5.object(eventos.pos, self.lista25)                
    				 	self.cubo6.object(eventos.pos, self.lista26)
    					if self.flechader.rect.collidepoint(eventos.pos) == 1:
    						if self.xcant>self.x6+1:
    							self.x1 += 6
    							self.x2 += 6
    							self.x3 += 6
    							self.x4 += 6
    							self.x5 += 6
    							self.x6 += 6
    					
    					if self.flechaizq.rect.collidepoint(eventos.pos) == 1:
    						if self.x1 != 0:
    							self.x1 -= 6
    							self.x2 -= 6
    							self.x3 -= 6
    							self.x4 -= 6
    							self.x5 -= 6
    							self.x6 -= 6
    

    Y esta es la clase Cubo (de la cual heredan cubo1, cubo2…):

    class Cubo(pygame.sprite.Sprite):
    	def __init__(self, x, y, z):
    		pygame.sprite.Sprite.__init__(self)
    		self.image = graphics.load_image("Imagenes\\"+ x + ".jpg", False)
    		self.rect = self.image.get_rect()
    		self.rect.centerx = y
    		self.rect.centery = z
    	def object(self,x, y):
    		if self.rect.collidepoint(x) == 1:
    			os.startfile(y + ".txt")
    
    
  18. adrigm dice:

    No deberías usar el gestor de eventos para obtener los eventos de entrada que necesitan manejarse en tiempo real, miráte esto:

    http://www.pygame.org/docs/ref/mouse.html

  19. julian9512 dice:

    Muchisimas gracias, a pesar de que ya habiavisto esa pagina y conocia un par de esos comando, nunca se me hubiera ocurrido que al aplicarlos se corregiria.
    Sos lo mas grande.
    Gracias y buen post.

  20. julian9512 dice:

    Disculpa, pero te molesto de vuelta.
    Ahora la duda es yo tengo una lista que busca todos elementos en un directorio especificsdo. Los elementos de esta se conforman con el directorio y el nombre y extension del archivo el problrma esta en que yo no quiero la direccion, solo el nombre y extension.
    El problema esta en que no se como extraer lo que no quiero de este elemento (el directorio)
    Espero tu resluesta y si no se entendio avisa y lo explico mejor.

  21. adrigm dice:

    Si no tiene que ver con el tema del post, mejor abre un tema en el foro y alli te podré ayudar para que todo tenga un orden.

    Por lo que entiendo quieres extraer el nombre de un archivo de una ruta, tengo una función para ello, mañana te la pongo que la tendo en el otro ordenador, pero si la quieres hacer tu basicamente consiste en recorrer la cadena de la ruta y buscar la posición del ultimo caracter “/” o “\” que son los caracteres de directorios a partir de ahi será el nombre del archivo, mañana si te lo pongo en el tema del foro que crees si no encuentras la solución.

  22. adrigm dice:

    Mira la tenía en Dropbox:

    # Extra el name de un archivo de una ruta.
    def extract_name(ruta):
        a = -1
        for i in range(len(ruta)):
            if ruta[i] == "/" or ruta[i] == "\\":
                a = i
        if a == -1:
            return ruta
        return ruta[a+1:]
    
  23. julian9512 dice:

    Puede ser que haya algo mal porque cuando lo aplico lo que me dice es el nombre del disco(en mi caso = G). Y encima lo que me confunde mas es que al escribir esto el resultado que me imprime es los mismo q al principio(“wow”):

    wow = (“G:\\hola\\hol.exe”)

    def extract_name(ruta):
    a = -1
    for i in range(len(ruta)):
    if ruta[i] == “/” or ruta[i] == “\\”:
    a = i
    if a == -1:
    return ruta
    return ruta[a+1:]
    while True:
    exe = extract_name(wow)
    print exe

  24. julian9512 dice:

    No te preocupes por la anterior pregunta ya lo arregle.

    Ahora el problema esta en cuando se lla ma desde una escena a def_change para cambiar. Mi primera pregunta es, ¿En vez de llamar a director.change_scene(scene), no se deberia llamar a director.Director.change_scene(scene)?, ya que def_change esta dentro de la clase Director.
    Y mi segunda pregunta es, yo tengo esto dentro da la clase Menu(lo primero que se ve) Alli hay un string y yo quiero que al apretarlo se cargue la otra escena hasta ahora tengo esto:

    def on_event(self):

    mouse1, mouse2, mouse3 = pygame.mouse.get_pressed()
    mouse_pos = pygame.mouse.get_pos()

    if mouse1 == 1:
    if self.filmshow_rect.collidepoint(mouse_pos) == 1:
    director.Director.change_scene(filmshow.Filmshow(dir))

    El problema esta en que python me devuelve cuando lo llamo esto:

    AtributeError: “Module” object has no atribute “Filmshow”

    ¿Qué es lo estoy pasando por alto?

  25. adrigm dice:

    Mirate el constructor de la clase Scene (la clase de la que heredan todas las escenas) tiene una referencia al objeto director para acceder al cambio de escena, por lo que se accede con self.director.change_scene()

    Al contrario que en el otro caso, si es para pinchar en un texto supongo que estarás haciendo un menú, no necesitas gestión en tiempo real por lo que deberías tratarlo como evento.

    Y por favor, si no tiene que ver con el post utiliza el foro, no te podré ayudar más aquí con temas no relacionados con el post.

  26. David dice:

    Para usar la función quit() de Director desde otra clase, ¿esa clase deberia heredar de Director?

  27. […] opciones, y todas las demás que se necesiten. Una buena referencia en este tópico se encuentra en Razón Artificial (Favor de leer la referencia dado que entraré de lleno a modificar el código para incluir […]

  28. […] Gestionando Escenas con Pygame […]

  29. […] we start, the article can be found here, if you are interested and can read it (more info at the […]

  30. fvparg dice:

    Hola, tengo una consulta, adrigm, sigues aquí? Gracias

Deja un comentario