Curso de desarrollo de videojuegos con Python

Componentes esenciales y básicos de un videojuego

Level 5: Bucle principal, actualización, renderizado y control de FPS con Pygame

En esta ocasión, nos centraremos en cuatro aspectos principales: el bucle principal del juego, la actualización del estado del juego, la representación de objetos en la pantalla y la gestión de la velocidad de fotogramas.

El bucle de juego en Pygame

El bucle de juego de Pygame, se ejecuta continuamente y asegura que el juego se actualice y se muestre correctamente en la pantalla. Sin este bucle, el juego se cierra irremediablemente. Es necesario, tal y como expliqué en el capítulo anterior, para mantenerlo en ejecución.
Además, si el bucle está mal colocado o hacemos algo que interfiera en él, tendremos fallos de actualización de los elementos en la pantalla, cosas que puede que no aparezcan y diversos fallos inesperados.

Aquí tienes el bucle que utilizamos en el capítulo anterior. Si lees otros tutoriales de Pygame, verás que se puede construir de varias formas. Tampoco vamos a entrar en detalles si una es mejor que otra. Esto lo dejo a vuestro juicio con las dos opciones que os voy a dar. Ya que realmente, ambas opciones van a ser válidas.

La que ves a continuación, es la forma que utilizaré habitualmente.

# Bucle para mantener el juego en marcha
while True:
    for evento in pygame.event.get():
        if evento.type == pygame.QUIT:
            pygame.quit()
            exit()

Se trata de un estilo compacto con todo lo necesario dentro del bucle.

Otras como la que ves en el siguiente ejemplo, utilizan una variable para el valor booleano en lugar de utilizarlo en la expresión del while:

# Variable de control booleana para el estado del bucle
ejecucion = True
											
# Bucle para mantener el juego en marcha
while ejecucion:
    # Control de eventos
    for evento in pygame.event.get():
        if evento.type == pygame.QUIT:
            ejecucion = False

Ambos fragmentos de código tienen el propósito de mantener el juego en ejecución y permitir su cierre adecuado cuando el jugador decide salir. Sin embargo, hay algunas diferencias a tener en cuenta entre ellos en términos de estructura y manejo de eventos.

El primer ejemplo (el que vamos a utilizar normalmente), utiliza un bucle infinito (while True) para mantener el juego en funcionamiento. Dentro de este bucle, se recorren todos los eventos de pygame (pygame.event.get()) y, si se detecta un evento de cierre (pygame.QUIT), se cierra adecuadamente el juego llamando a pygame.quit() y exit().

En cambio, en el segundo ejemplo, se utiliza una variable booleana llamada ejecucion como condición del bucle (while ejecucion). La variable se inicializa en True y el bucle se ejecuta mientras ejecucion siga en True. Dentro del bucle, también se recorren los eventos y, si se detecta un evento de cierre. Cuando se tiene que finalizar el juego, se cambia el valor de ejecucion a False, lo que eventualmente detendrá el bucle y cerrará el juego.

¿Cuál de las dos formas es más óptima para definir los bucles de Pygame?

En cuanto a que forma de crear este tipo de bucle es más óptima, en términos de rendimiento, ambos códigos son equivalentes y no hay una diferencia significativa. La elección entre ellos depende más de la preferencia y estructura de programación que desees emplear en tu proyecto.
A mí me parece mejor tenerlo todo recogido en el bucle. En cambio, con el segundo ejemplo, necesitamos un elemento externo al bucle, la variable ejecucion. Aunque está bien si crees que le aporta sencillez a la interpretación del código.

Con esto, quiero que quede clara una cosa importante. El bucle del juego, es un componente vital y hay que cuidarlo. La mayoría de fallos en el juego, vendrán por problemas con el bucle de juego.

Bucle infinito para juegos

En la comparación anterior, he explicado brevemente las partes del bucle con while True. Vamos a ver cada trozo del bucle, parte por parte.

Es importante comprender la estructura básica del bucle principal. Utiliza un bucle while que se ejecuta indefinidamente hasta que se cumple la condición de salida. Dentro del ciclo, se procesan los eventos de Pygame que capturan las acciones del jugador, como los clics del mouse o las pulsaciones de teclas. Estos eventos permiten que el juego reaccione a las acciones del jugador y actualice su estado en consecuencia. Veamos cada parte:

  1. while True: constituye un bucle infinito, lo que significa que se ejecutará continuamente hasta que se detenga explícitamente. En este caso se utiliza para mantener activo el bucle principal del juego for.
  2. for evento in pygame.event.get(): itera a través de todos los eventos que ocurren en una ventana usando el método pygame.event.get(). Cada evento se almacena en una variable de evento y el bloque de código en el ciclo for se ejecuta para manejar los eventos por separado.
  3. if event.type == pygame.QUIT: comprueba si el tipo de evento es pygame.QUIT, lo que indica que el usuario está intentando cerrar la ventana. Si se cumple esta condición, significa que el usuario quiere salir del programa y el bloque de código se ejecutará bajo esta condición.
  4. pygame.quit(): esta función se usa para salir de Pygame correctamente. Cierra subsistemas y libera recursos utilizados por la biblioteca. En este caso, se llama a pygame.quit() para asegurarse de que pygame salga correctamente cuando el usuario intente cerrar la ventana.
  5. exit(): con esta función de Python, el programa termina completamente en este punto. Llamar a exit() detiene el programa inmediatamente y cierra el script de Python por completo. En este caso, se usa después de llamar a pygame.quit() para asegurarse de que el programa Python se cierra por completo después de que Pygame se cierra.

El bucle principal es responsable de dos tareas principales: actualizar el estado del juego con todo lo que ello conlleva y dibujar objetos en la pantalla. Con cada iteración del bucle, se actualizan las variables y las estructuras de datos que representan el estado del juego. Esto incluye reposicionamiento de personajes, actualizaciones de puntos de jugador, detección de colisiones y mucho más. Los objetos se tienen que ir dibujando y actualizando en la pantalla para que el jugador los vea. Imagina ese pequeño truco de dibujo de fotogramas en una libreta, en la cual vas pasando las hojas muy rápido para crear la sensación de animación.

Sprite de mago corriendo

Actualización del estado del juego

Actualizar correctamente el estado del juego es una parte importante para crear una experiencia dinámica e interactiva. La lógica del juego define cómo se comporta y cómo se juega. Aquí incluimos cosas como el objetivo, las condiciones de ganar/perder y la interacción del jugador.
Por ejemplo, cuando desarrollamos un juego de plataformas, actualizamos la posición y la velocidad del personaje en función de las acciones del jugador. También verificamos si hay colisiones con otros objetos del juego y se toman las medidas apropiadas, tales como perder una vida, o parte de ella. Así como mostrar alguna animación al respecto y algún sonido de dolor, derrota, victoria, etc.

La actualización del estado del juego también afecta al movimiento de los objetos, tanto los personajes controlados por el jugador como los elementos del entorno o los enemigos. Esto incluye el cálculo de nuevas posiciones en función de las velocidades y las interacciones con otros objetos. Para esto, necesitamos de algoritmos matemáticos que hagan los cálculos por nosotros.

Todo esto puede parecer súper complicado. De hecho, lo es, pero verás que con Pygame, se nos facilitan mucho estas tareas en comparación a otros entornos de desarrollo.

Dibujar objetos en la pantalla

Después de actualizar el estado del juego, es muy importante mostrar objetos en la pantalla para que los jugadores puedan verlos. Renderizar objetos en Pygame requiere trabajar con la ventana del juego y los sprites.

La ventana del juego es la pantalla donde se muestran los gráficos del juego. Se crea usando la función pygame.display.set_mode() y tiene un tamaño, un icono y un título. Una vez que la ventana está lista, podemos usar las diversas funciones de renderizado de Pygame para dibujar y animar objetos sobre ella.

Las superficies o formas geométricas

Para dibujar formas básicas como rectángulos o círculos se utilizan lo que se conoce como superficies. Una superficie es un lienzo virtual en el que podemos dibujar formas y luego mostrarlas en una ventana. Esto lo conseguimos con funciones como pygame.draw.rect() para el rectángulo o pygame.draw.circle() para el círculo. Después, establecemos las propiedades de las formas como la posición, el tamaño y el color y las dibujamos en la superficie.

Exterminator en Pygame

Muchos juegos suelen utilizar imágenes para representar objetos más complejos, como personajes, enemigos o elementos ambientales. Así que no te preocupes, porque no tendrás que dibujar con código parte a parte a los personajes. Más bien, los cargaremos y mostraremos.
Pygame proporciona funciones para cargar y mostrar imágenes en la pantalla.
En el juego que ves en la imagen, solo hay dos imágenes superpuestas. Una para el personaje y otra para el fondo.
Se puede usar la función pygame.image.load() para cargar una imagen desde un archivo y luego usar la función window.blit() para mostrarla en la ventana en la posición especificada. En el caso del icono que cargamos en el capítulo anterior, no es necesario este blit().

Además, podemos superponer objetos para controlar su orden de renderizado y aplicar transformaciones visuales como rotación o escalado. Esto nos permitirá crear efectos visuales interesantes y mejorar la apariencia del juego.

Control de velocidad de fotogramas

El control de velocidad de fotogramas (FPS) es un aspecto importante del desarrollo de juegos para garantizar una experiencia fluida y estable.
La velocidad de fotogramas es el número de imágenes (fotogramas) que se muestran por segundo en un juego. Una velocidad de fotogramas baja puede hacer que el juego se sienta lento y entrecortado, mientras que una velocidad de fotogramas alta puede consumir una gran cantidad de recursos del sistema y acelerar demasiado el juego. Hay que encontrar un equilibrio óptimo para cada juego.

Pygame proporciona herramientas para una gestión eficiente de la velocidad de los fotogramas. Mediante la función pygame.time.Clock(), se puede crear un objeto de tipo reloj que nos ayudará a controlar el tiempo en el juego. Luego usamos el método clock.tick() para limitar la velocidad de fotogramas del juego a un cierto valor.

El control de velocidad de fotogramas también incluye el tiempo entre fotogramas. Esto nos permite lograr animaciones fluidas y consistentes en diferentes sistemas, independientemente de la velocidad del juego. Para ello, se puede utilizar la función pygame.time.get_ticks(), que nos proporcionará el tiempo transcurrido en milisegundos desde que se inició el juego.

Dejamos el capítulo aquí. Un poco más técnico de lo habitual, pero totalmente necesario. Si no logras entender del todo para qué necesitamos saber todo esto, no te preocupes, habrá capítulos dedicados al manejo de estas cosas. Esto es solo una visión general de los próximos capítulos.


Comentarios

Si te quedan dudas sobre el temario, sobre Python, los videojuegos, cualquier otra cosa relacionada o simplemente quieres agradecer, aquí tienes tu sitio para dejar tu granito de arena. Gracias por tus comentarios y por darle vida a este sitio web.

Programación Fácil YouTube

Suscríbete

Si te ha gustado este curso y crees que el trabajo merece la pena, te agradeceré eternamente que te suscribas a mi canal de YouTube para apoyarme y que pueda seguir haciendo cursos gratuitos.

Además, si te encanta la programación, tienes un montón más de cursos gratuitos para ver.

No solo eso, podrás participar enviándome comentarios con tus sugerencias para temas específicos o cursos completos o incluso las dudas que tengas y las intentaré ir resolviendo en los cursos que estén todavía abiertos.