Programacion en 2D
Programacion en 2D
Programacion en 2D
2
Programación en 2D
■ En principio no son muchos y dependerán básicamente del tipo de juego, pero suelen ser esenciales conocimientos básicos de trigonometría y geometría.
○ Conocimientos de física.
■ Como las matemáticas, depende del tipo de juego, para juegos de plataforma con conocimientos básicos de cinemática es suficiente.
○ Conocimientos de programación.
■ Del lenguaje elegido para programar lo más importante es que sea popular con una amplia comunidad y colección de bibliotecas.
● Consulta
● Recursos
● No obstante, esta menor velocidad no resulta en absoluto importante para los videojuegos en 2D.
■ El modelo de memoria de Python es sencillo: todo valor reside en el heap y toda variable contiene una referencia a su
Lenguaje Biblioteca
valor.
C SDL
■ A ello se suma un sistema de recogida automática de basura (garbage collection) que evita los punteros colgantes
C++ SFML
(dangling pointers), las fugas de memoria, las violaciones de segmento, etc.
● Estos errores de ejecución, habituales en lenguajes como C o C++, son difíciles de detectar y pueden convertir el C# XNA / MonoGame
desarrollo de programas en una actividad tediosa y frustrante
Python PyGame
○ Python ofrece una amplia colección de módulos (bibliotecas)
Java libgdx, spiller
■ Hay módulos para cualquier actividad imaginable:
Ruby Gosu
● Escritura de CGI, gestión de correo electrónico, desarrollo de interfaces gráficas de usuario, análisis de documentos
HTML o XML, acceso a bases de datos, trabajo con expresiones regulares, etc. Flash Flixel
○ El mecanismo de paso de parámetros es único. Lua Love2D
■ Los parámetros se pasan a funciones y métodos por referencia a objeto.
○ El paso de objetos básicos (de tipo escalar) se realiza con efectos similares al paso por valor
○ El de objetos más elaborados (vectores, diccionarios, instancias de clase, etc.) por referencia.
● Python + Pygame
○ Ventajas
■ Libres
■ Multiplataforma
○ Desventajas
● Pygame es una biblioteca multimedia (que trabaja sobre las librerías SDL) que permiten la creación de
videojuegos en dos dimensiones de una manera sencilla.
○ Las SDL (Simple DirectMedia Layer) son un conjunto de bibliotecas que proporcionan funciones básicas para realizar operaciones de dibujado 2D, gestión de efectos de sonido,
música, carga y gestión de imágenes.
■ Pygame puede ser usado también para la realización de aplicaciones multimedia e interfaces gráficas.
● Básicamente, pygame es una librería similar a OpenGL o DirectX (sólo en la parte 2D), diseñada para ser
más fácil de usar que estas otras
○ Además, OpenGL solo se encarga de la parte gráfica, en cambio SDL incluye otras cosas como son los sonidos
● El desarrollo de PyGame comenzó en el 2000 de la mano de Pete Shinners que acababa de conocer
Python y SDL (Simple Directmedia Layer), así como un pequeño proyecto denominado PySDL que
pretendía fusionar ambos
○ La desparición de PySDL motivó la decisión de asumir el proyecto de realizar un motor de juego basado en Python y SDL.
■ Por tanto, básicamente uno se tiene que preocupar por la programación del juego en sí
■ PyGame permite la creación y desarrollo de videojuegos de una forma clara y sencilla, sin que esto nos impida obtener los resultados esperados puesto que puede
combinarse con otras librerías, como PyOpenGl, para incluir gráficos 3D.
● Otra de las características de Python es la existencia de una completa documentación para facilitar la
comprensión y utilización, así como una gran variedad de ejemplos y una amplia comunidad de usuarios
● El bucle de juego
○ Bucle infinito que ejecuta las diferentes fases del juego y controla el gameplay
○ 3 fases:
■ Importación
■ Inicialización
■ Ejecución
El módulo sys es importante para trabajar correctamente con el sistema Constantes y variables útiles
operativo. En particular, sys.exit()
● Inicialización pygame.init()
visor = pygame.display.set_mode((640, 480), 0, 32)
Opciones
● Inicialización
○ Opciones: Flags (separadas por | )
pygame.init()
■ FULLSCREEN: Pantalla completa visor = pygame.display.set_mode((640, 480), 0, 32)
■ RESIZABLE: Tamaño variable
■ OPENGL: Para 3D
■ 8: 256 colores
○ Visor contiene un objeto Surface que puede ser una ventana o la pantalla completa
■ Importación
■ Inicialización
■ Ejecución
● Dibujado en pantalla:
○ Para evitar artefactos se dibuja offscreen en un buffer
● Dibujar imágenes
● Operación de blitting
● El blitting es una operación para transferir un bloque de bytes de un sector de la memoria a otra
● Este término viene del término inglés blit (Block Image Transfer, Transferencia de Imagen por Bloque) y es
una forma de dibujar imágenes con o sin transparencias sobre un fondo
○ Este fondo normalmente es un buffer que representa a la propia pantalla
● Esta técnica puede ser acelerada por hardware, lo cual ayuda a incrementar el rendimiento
○ De hecho es una de las operaciones más críticas en cualquier juego
● En una operación de blitting, se puede especificar en qué posición de la imagen de destino se quiere
copiar la imagen origen
○ Incluso qué parte de la superficie origen se quiere copiar.
○ De esta forma se puede colocar en cualquier posición, unas imágenes dentro de otras.
● Normalmente, y para que todo vaya rápido, se monta la escena primero en un buffer mediante
esta operación, y se coge la imagen resultante y se manda a la pantalla
○ De esta manera, en pantalla sólo se dibuja una única vez por fotograma del videojuego
■ Lo cual es bastante más rápido que pintar varias veces en pantalla por cada fotograma
● En Pygame, se tiene un buffer que es el buffer de pantalla, donde se dibuja toda la escena, y
posteriormente se manda a la pantalla este buffer utilizando esta técnica, mediante la función
anterior
○ ¿Qué ocurriría si se dibujase directamente en pantalla?
■ Imágenes
○ draw.line()
■ Líneas
○ draw.circle()
■ Círculos
○ draw.ellipse()
■ Elipses
○ draw.rect()
■ Rectángulos
○ etc.
● Colores:
○ Tuplas con los componentes Rojo, Verde y Azul (RGB)
● Posiciones:
○ Tuplas con las coordenadas (x, y)
● Zonas:
○ Tuplas con la posición de arriba a la izquierda, largo y alto
visor.blit(texto, textRect)
■ Ejemplo:
BLANCO = (255,255,255)
# Actualizamos la pantalla
pygame.display.update()
● En PyGame
while True:
...
for event in pygame.event.get():
...
if event.type == QUIT:
pygame.quit( ) Mirar la lista
sys.exit( ) de eventos
# Actualizamos la pantalla
pygame.display.update() 2 - con el bucle infinito.py
# Actualizamos la pantalla
pygame.display.update()
# Posicion de la pelota
pelotaX = 50
pelotaY = 50
while True:
pantalla.fill((0,0,0))
pygame.draw.circle(pantalla, BLANCO, (pelotaX,pelotaY),4,0)
pygame.display.update()
3 - la pelota se mueve (muy rapido).py
# Posicion de la pelota
pelotaX = 50
pelotaY = 50
while True:
pantalla.fill((0,0,0))
pygame.draw.circle(pantalla, BLANCO, (pelotaX,pelotaY),4,0)
pygame.display.update()
■ Se garantiza que cada iteración del ciclo (cada frame) se realiza cada cierto tiempo (milisegundos)
● Especialmente en juegos en 2D
○ ¿Cómo se hace?
● Típicamente, al inicio
■ La próxima vez que se llega a ese punto se espera hasta que transcurre el tiempo correspondiente
● Así se consigue que el bucle se ejecute un número determinado de veces por segundo
● Si el equipo es muy potente, solo funcionará a los FPS especificados (pese a que puede ir mas rápido)
○ Inconveniente:
■ Si se usa este método en una máquina con un procesador mucho más antiguo, lo más probable es que vaya bastante lento
● Requerimientos mínimos
■ Lo que se hace es calcular la posición de un objeto en función del tiempo transcurrido desde el ciclo anterior del bucle
■ Se mueven de igual manera los objetos sin importar en qué equipo se ejecute el juego
● Con el primer método (FPS) si se quiere mover un objeto 8 pixeles, se haría lo siguiente:
○ x=x+8
○ Por ejemplo si se desplaza el objeto a una velocidad de 0.008 píxeles/seg. , y el ciclo demora 1 segundo en ejecutarse (1000ms), el nuevo incremento será de:
■ x = x + 0.008*1000
■ x=x+8
■ En las escenas complejas es posible que una máquina no pueda visualizar la escena a un determinado FPS
● Va a saltos
# Bucle infinito
while True:
pelotaX += 2
pelotaY += 2
pantalla.fill((0,0,0))
pygame.draw.circle(pantalla, BLANCO, (pelotaX,pelotaY),4,0)
pygame.display.update()
4 - con reloj.py
● Método clock.tick():
○ Si se usa sin argumentos (o sea, clock.tick()) devuelve el tiempo que ha pasado (en milisegundos) desde la ultima vez que se llamó
○ Si se usa con un argumento, que es el framerate, la función esperará el tiempo necesario para mantener al juego corriendo a la velocidad solicitada
■ Es decir, si se llama clock.tick(60), el juego nunca correrá a más de 60 frames por segundo
■ clock.tick_busy_loop
# Bucle infinito
while True:
Sincronización por FPS
# Hacemos que el reloj espere a un determinado fps Se espera hasta el siguiente
tiempo = reloj.tick(60) frame, devuelve el tiempo
transcurrido
# Para cada evento posible
for evento in pygame.event.get():
if evento.type == KEYDOWN and evento.key == K_ESCAPE:
pygame.quit()
sys.exit()
Sincronización por tiempo
pelotaX += tiempo * VELOCIDAD_PELOTA_X Se calcula la posición en función
pelotaY += tiempo * VELOCIDAD_PELOTA_Y
del tiempo transcurrido
pantalla.fill((0,0,0))
pygame.draw.circle(pantalla, BLANCO, (pelotaX,pelotaY),4,0)
pygame.display.update()
● Invertir la velocidad
# Actualizamos la pantalla
pygame.display.update()
6 - con las raquetas.py
■ Los eventos se pueden producir en cualquier momento, esté haciendo lo que esté haciendo la aplicación
● Se comprueban los eventos en la cola una vez cada ciclo del bucle
■ Después, actualizar los objetos en función de los eventos que hayan llegado (y de lo que ocurra
en el juego)
● Eventos
○ Para recoger los eventos
■ pygame.event.get()
■ pygame.event.wait()
■ pygame.event.poll()
● Eventos
○ Además, se puede filtrar los eventos con:
■ pygame.event.set_allowed()
■ pygame.event.set_blocked()
● type:
■ Ejemplos anteriores
○ Función pygame.key.get_pressed()
■ Con el primer método se produce un solo evento cuando se pulsa, y otro cuando se suelta
■ ¿Cómo utilizar el primer método para generar múltiples eventos cuando se mantiene una tecla pulsada?
● pygame.key.set_repeat()
○ Se le puede indicar el tiempo (ms) que tiene que transcurrir con la tecla pulsada para que se genere otro evento
■ Cuidado:
● Debido a limitaciones del hardware, algunas combinaciones de teclas no pueden ser detectadas
■ Función pygame.key.get_pressed()
● Por ejemplo:
teclas_pulsadas = pygame.key.get_pressed()
if teclas_pulsadas[K_SPACE]:
fire()
■ pygame.key.get_focused
● Devuelve un valor booleano que dice si la ventana tiene el foco, y puede recibir entrada de teclado
■ pygame.key.name
● Recibe como parámetro una constante KEY_ y devuelve un string con el nombre de la tecla, como ‘a’, ‘b’, etc.
■ pygame.key.get_mods
● Devuelve un valor que dice cuál de las teclas modificadoras (Shift, Alt, Ctrl) están pulsadas
■ pygame.key.set_mods
○ Por ejemplo, para usar las teclas Shift y Alt: pygame.key.set_mods(KMOD_SHIFT | KMOD_ALT)
● Eventos:
○ pygame.MOUSEMOTION
○ pygame.MOUSEBUTTONDOWN
○ pygame.MOUSEBUTTONUP
● pos: posición
○ 4: rueda arriba
○ 5: rueda abajo
○ Posición:
■ pygame.mouse.get_pos()
● Devuelve la posición
● Además, pygame.mouse.set_pos()
■ pygame.mouse.get_rel()
○ Botones pulsados
■ pygame.mouse.get_pressed
● Devuelve (button1, button2, button3) booleanos representando el estado de los 3 botones del ratón
■ Además:
● pygame.mouse.set_visible(False)
● pygame.event.set_grab(True)
○ Los movimientos relativos del ratón nunca se pararán por los bordes de la pantalla
● pygame.mouse.set_cursor
● pygame.mouse.get_cursor
■ Funciones:
● pygame.joystick.init
● pygame.joystick.quit
● pygame.joystick.get_init
■ Funciones:
● pygame.joystick.get_count
● pygame.joystick.Joystick
○ Crea un nuevo objeto joystick que se usa para acceder a toda la información de un periférico determinado.
■ pygame.joystick.Joystick.init()
● JOYBUTTONDOWN, JOYBUTTONUP
○ Pulsación de botones
● JOYAXISMOTION
○ Movimiento de sticks
● JOYBALLMOTION
○ Movimiento de bolas
● JOYHATMOTION
○ Pulsación de crucetas
● JOYBUTTONDOWN, JOYBUTTONUP
○ Contienen:
● Se puede saber el número de botones con el método get_numbuttons del objeto Joystick
● JOYAXISMOTION
○ Sticks analógicos
○ Contiene:
● 0: eje x
● 1: eje y
● JOYAXISMOTION
○ Problema: Debido a la naturaleza mecánica y analógica de los sticks, se puede producir un flujo constante de eventos JOYAXISMOTION sin tocar nada
● JOYBALLMOTION
○ Contiene:
■ joy: ID
● JOYHATMOTION
■ Para gestionar el joystick (Obtener el estado del joystick: Mirar el objeto Joystick (1/3))
● pygame.joystick.Joystick.init
○ Inicializa el Joystick
● pygame.joystick.Joystick.quit
○ Finaliza el Joystick
● pygame.joystick.Joystick.get_init
● pygame.joystick.Joystick.get_id
● pygame.joystick.Joystick.get_name
■ Para gestionar el joystick (Obtener el estado del joystick: Mirar el objeto Joystick (2/3))
● pygame.joystick.Joystick.get_numaxes
● pygame.joystick.Joystick.get_axis
● pygame.joystick.Joystick.get_numballs
● pygame.joystick.Joystick.get_ball
■ Para gestionar el joystick (Obtener el estado del joystick: Mirar el objeto Joystick (3/3))
● pygame.joystick.Joystick.get_numbuttons
● pygame.joystick.Joystick.get_button
■ Valor booleano
● pygame.joystick.Joystick.get_numhats
● pygame.joystick.Joystick.get_hat
■ Tupla (x, y), digital, cada valor sólo puede ser -1, 0 o 1
■ Por ejemplo:
● Si se mueve con velocidad a la derecha o abajo, se moverá en esta dirección esa velocidad
x
45
x = cos(45) = 0.707
1 y
y = sen(45) = 0.707
■ Detectar cuando un jugador falle, hacer una pausa y reiniciar: poner la pelota en el centro, hacia el otro jugador
# Actualizamos la pantalla
pygame.display.update()
13 - marcadores.py
● Sonidos
○ Uso del módulo pygame.mixer
● pygame.mixer.init
■ Antes de eso, hay que indicar los parámetros con la función pygame.mixer.pre_init:
■ Por ejemplo:
● Sonidos
○ Uso del módulo pygame.mixer
● Frecuencia
● Tamaño
○ Calidad
● Estéreo
○ 1: mono, 2: estéreo
● Buffer
● Sonidos
○ Uso del módulo pygame.mixer
● Buffer
■ Potencia de dos
● Sonidos
○ Para cambiar los parámetros
■ Hay que realizar una llamada a pygame.mixer.quit
■ Posteriormente, otra a pygame.mixer.pre_init y después pygame.mixer.init
■ Pygame.mixer.Sound
■ Por ejemplo, si es 5, cuando se reproduzca se repetirá 5 veces (se escuchará 6 veces en total)
● Ejemplo:
sonido = Pygame.mixer.Sound(“sonido.ogg")
● Sonidos
○ Una vez cargado un sonido, se puede llamar al método play
■ Ejemplo:
sonido = Pygame.mixer.Sound(“sonido.ogg")
canal = sonido.play()
● Por ejemplo, si se tiene un sonido de aplausos que dura 1.2 segundos y se quiere reproducir durante 5 segundos:
sonido_aplausos = Pygame.mixer.Sound(“aplausos.ogg")
canal = sonido_aplausos.play(-1, 5000)
● Sonidos
○ Métodos de los objetos de sonido:
■ fadeout
■ get_length
■ get_num_channels
● Sonidos
○ Métodos de los objetos de sonido:
■ play
● Reproduce el sonido
■ stop
■ get_volume
● Devuelve el volumen del sonido, como un número real entre 0 (silencio) y 1 (mayor volumen)
■ set_volume
● Sonidos
○ Si la reproducción (llamada a play) se realiza, se devuelve una instancia de Channel
■ Un canal es una de varias fuentes de sonido que son mezcladas juntas por la tarjeta de sonido
● pygame.mixer.get_num_channels
● pygame.mixer.set_num_channels
● Sonidos
○ Objetos Channel:
● Crea la ilusión de que un sonido viene de un punto particular de la ventana (stereo panning)
(ambos entre 0 y 1)
● Sonidos
○ Objetos Channel:
■ Implementan una cola de tal forma que se pueden poner sonidos para su reproducción cuando termine el anterior
● Método queue
● El método set_endevent recibe como parámetro un número de evento que será enviado cuando termine la reproducción
● Este id del evento debería ser superior a USEREVENT para evitar conflictos con el resto de eventos de pygame
● Sonidos
○ Generalmente lo mejor es dejar a pygame la selección del canal, pero se puede forzar a un objeto Sound a ser reproducido en un
determinado canal mediante el método play de ese canal
■ A ese método se le pueden pasar los parámetros de número de veces que se repetirá y duración
● Por ejemplo, que los efectos de sonido de un juego (por ejemplo, disparos) no se vean bloqueados por el sonido de fondo
● Sonidos
○ Para reservar un canal: pygame.mixer.set_reserved
■ Por ejemplo:
pygame.mixer.set_reserved(2)
canal_reservado_0 = pygame.mixer.Channel(0)
canal_reservado_1 = pygame.mixer.Channel(1)
canal_reservado_1.play(gunfire_sound)
● Sonidos
○ Métodos de la clase Channel (1/4):
■ play
● Toma como parámetros un objeto Sound y valores opcionales de repetición y tiempo máximo
■ stop
■ pause
■ unpause
● Sonidos
○ Métodos de la clase Channel (2/4):
■ set_volume
■ get_volume
● Devuelve el volumen actual del canal como un valor entre 0 y 1 (no toma en consideración el volumen estéreo puesto en
set_volume)
■ fadeout
● Sonidos
○ Métodos de la clase Channel (3/4):
■ get_busy
■ queue
■ get_queue
● Devuelve cualquier sonido que se haya puesto en la cola para reproducción, o None si no hay ninguno
● Sonidos
○ Métodos de la clase Channel (4/4):
■ set_endevent
■ get_endevent
● Devuelve el evento que será enviado cuando un sonido se termine de reproducir, o NOEVENT si no se ha fijado un evento
● Sonidos
○ Métodos de pygame.mixer (1/4):
■ pygame.mixer.pre_init
■ pygame.mixer.init
■ pygame.mixer.get_init
■ pygame.mixer.quit
● Finaliza el módulo.
● Se llama automáticamente cuando se finaliza el script, pero es necesario llamarlo para reiniciarlo con parámetros diferentes
● Sonidos
○ Métodos de pygame.mixer (2/4):
■ pygame.mixer.get_num_channels
■ pygame.mixer.set_num_channels
■ pygame.mixer.Channel
■ pygame.mixer.find_channel
● Sonidos
○ Métodos de pygame.mixer (3/4):
■ pygame.mixer.Sound
■ pygame.mixer.get_busy
■ pygame.mixer.fadeout
● Sonidos
○ Métodos de pygame.mixer (4/4):
■ pygame.mixer.pause
■ pygame.mixer.unpause
■ pygame.mixer.stop
● Sonidos
○ Música:
○ Si se hiciese con las funciones anteriores, obligaría a tener todo el archivo de música en memoria
○ Submódulo pygame.mixer.music
■ Voz
● Sonidos
○ Música:
● pygame.mixer.music.load
● pygame.mixer.music.play
● Ejemplo:
pygame.mixer.music.load(“musica.ogg")
pygame.mixer.music.play()
● Sonidos
○ Funciones del módulo pygame.mixer.music (1/5):
■ pygame.mixer.music.load
■ pygame.muxer.music.queue
● Toma un único parámetro, que el es nombre del archivo que se quiere poner en cola
● Sonidos
○ Funciones del módulo pygame.mixer.music (2/5):
■ pygame.mixer.music.play
■ pygame.mixer.music.stop
■ pygame.mixer.music.rewind
● Sonidos
○ Funciones del módulo pygame.mixer.music (3/5):
■ pygame.muxer.music.pause
■ pygame.mixer.music.unpause
■ pygame.mixer.get_busy
■ pygame.mixer.music.get_pos
● Sonidos
○ Funciones del módulo pygame.mixer.music (4/5):
■ pygame.mixer.music.set_volume
■ pygame.mixer.music.get_volume
■ pygame.mixer.music.fadeout
● Sonidos
○ Funciones del módulo pygame.mixer.music (5/5):
■ pygame.mixer.music.get_endevent
■ pygame.mixer.music.set_endevent
● Toma como parámetro el identificador del evento, que debería de ser superior a USEREVENT
■ Buscador de sonidos
○ http://spriters-resource.com/
# Actualizamos la pantalla
pygame.display.update() 16 - con imagen de fondo.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
● Orientación a objetos
Raqueta Pelota
controlaY() update()
colision() dibuja()
dibuja()
○ Método update
# Comprobamos que ninguna de las dos raquetas se hayan ido por arriba o abajo
jugador1.controlaY()
jugador2.controlaY()
17 - orientacion a objetos.py
● Orientación a objetos
○ Los objetos que se muestran por pantalla son subclases de Sprite
■ Son Sprites
■ Un Sprite es cualquier objeto que aparece en el videojuego, aunque normalmente suele relacionarse a todo
personaje o animación dentro del mismo.
■ Normalmente tienen una serie de atributos, entre los más básicos están las imágenes que lo componen y su
posición.
Raqueta Pelota
18 - con sprites.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
● Sprites
class Raqueta(pygame.sprite.Sprite):
"Las raquetas de ambos jugadores"
18 - con sprites.py
● Para calcular si sus rectángulos se solapan, se puede utilizar el método de la clase Rect: colliderect
self.rect.colliderect(pelota.rect)
18 - con sprites.py
○ La colisión se comprueba entre los rectángulos que ocupan los Sprites en pantalla
■ Atributo rect
■ Sin embargo, puede ser útil definir varios atributos Rect para poder definir nuevos rectángulos de colisión además del principal
● Por ejemplo:
● Opcional
■ Mueve el Sprite
● realiza si lo es
■ Sprite
■ etc.
● Sprites
○ Las imágenes de los Sprites suelen tener una parte que es transparente, por ello, se marca
mediante el canal llamado: Canal Alfa, que indica qué partes de la imagen son transparentes y
cuáles opacas
■ Imágenes en RGBA
● RGB + Alpha
● Canal alfa
○ Normalmente los programas de dibujo y tratamiento de imagen suelen asociar el canal alfa
mostrando un fondo de cuadros y la imagen sobre este fondo, indicando que la imagen en esa
parte es transparente o translúcida.
● Canal alfa
○ Sin embargo, el canal alfa no solamente contiene información para cada píxel de transparente/opaco
● Formatos de imágenes
○ JPEG (Joint Photographic Expert Group): ○ PNG (Portable Network Graphics):
■ Formato diseñado para fotografías ■ Formato de imágenes muy versátil
■ Compresión que reduce el tamaño de la ■ Comprime muy bien el tamaño sin pérdida
imagen
de calidad
● Pero reduce también la calidad de la
imagen ■ Soporta canales alfa
○ Regla general:
● Sprites
○ Se puede tener dos tipos de Sprite en un juego
● En general, todos los Sprites que tengan algún tipo de comportamiento asociado
○ Aunque no se muevan
○ Por ejemplo: personajes, zonas en las que se pierde vida, plataformas que no se mueven pero cambian su imagen
conforme pasa el tiempo, etc.
■ Estáticos: se referirán a todos los elementos del juego que no sean el fondo de la pantalla y que no se muevan a lo largo del
juego
● Para dibujar un Sprite estático, basta con dibujar (blit) su imagen en una determinada posición
pantalla.blit(miSprite.image, miSprite.rect);
■ Nivel de transparencia
● Sin embargo, un Sprite dinámico debe de mostrar una animación
○ Por ejemplo, el caminar de un personaje
● Para animar un juego 2D, cada Sprite tiene asociado un array de imágenes
○ Estas imágenes se muestran secuencialmente cada cierto tiempo para dar sensación de animación.
● Estas hojas suelen montarse para no estar trabajando con cientos (o, a veces, miles) de imágenes.
● Para coger cada una de las imágenes que formarán la animación se puede utilizar la siguiente función:
● Con esto no se crea una nueva imagen, si no que se referencia sobre la anterior, siendo más eficiente (ya
que no se ocupa tanta memoria) y más óptimo.
■ Por tanto, estos Sprites suelen tener fondos chillones ya que es poco probable que se encuentre ese color en la imagen.
● Generalmente se toma como color base el color del píxel en la posición (0, 0) de la imagen
● Parte del fondo (cercana al objeto) no será del mismo color que el
fondo
○ No se reconocerán
como transparentes
■ Método update:
● Actualiza el estado general del Sprite
○ Qué subimagen de la hoja se va a mostrar (entre otras cosas)
● Es necesario, por tanto, conocer las coordenadas en la hoja de Sprite donde está cada frame
○ Por ejemplo:
(0, 0)
(5, 25) 34
40
● Es necesario, por tanto, conocer las coordenadas en la hoja de Sprite donde está cada frame
○ Por ejemplo:
(0, 0)
34
(42, 25)
40
● Estas dos condiciones facilitan el proceso de saber las coordenadas de cada frame
■ Una multiplicación sencilla
# Si la tecla es Escape
if teclasPulsadas[K_ESCAPE]:
pygame.quit()
sys.exit()
# Indicamos la acción a realizar segun la tecla pulsada
elif teclasPulsadas[K_LEFT]:
jugador.mover(IZQUIERDA)
elif teclasPulsadas[K_RIGHT]:
jugador.mover(DERECHA)
# Actualizamos el jugador
jugador.update()
2 – sprite animado.py
def update(self):
# Si vamos a la izquierda
if self.movimiento == IZQUIERDA:
# Actualizamos la posicion
self.posicionx -= 2
# Actualizamos la imagen a mostrar
self.numImagenPostura += 1
if self.numImagenPostura >= len(self.coordenadasHoja[self.numPostura]):
self.numImagenPostura = 0;
# Su siguiente movimiento (si no se pulsa mas) sera estar quieto
self.movimiento = QUIETO
# Si vamos a la derecha En cada llamada al método update se
elif self.movimiento == DERECHA: calcula en qué postura está el Sprite:
# Actualizamos la posicion
self.posicionx += 2
Se muestra la postura siguiente (incorrecto)
# Actualizamos la imagen a mostrar
self.numImagenPostura -= 1
if self.numImagenPostura < 0:
self.numImagenPostura = len(self.coordenadasHoja[self.numPostura])-1
# Su siguiente movimiento (si no se pulsa mas) sera estar quieto
self.movimiento = QUIETO
2 – sprite animado.py
● Un problema que ocurre es que la posición del Sprite en pantalla se da sobre la esquina superior izquierda
○ La coordenada y crece de arriba a abajo
○ Si el personaje tiene una vista lateral y hay animaciones con imágenes más pequeñas que otras, las cuadra sobre esta esquina haciendo que el Sprite “vuele”
■ Por ejemplo, si el Sprite está en la posición (x, y), y se dibuja en esa misma posición:
(x, y) (x, y)
● Un problema que ocurre es que la posición del Sprite en pantalla se da sobre la esquina superior
izquierda
○ Necesario recalcular la posición del Sprite en función de la altura que tiene:
h
h
(x, y) (x, y)
● Un problema que ocurre es que la posición del Sprite en pantalla se da sobre la esquina superior izquierda
○ Este problema podría darse, de forma similar, en el eje x, si es necesario alinear los sprites a la derecha
■ Si su coordenada x es la del lado izquierdo y se dibuja en esa coordenada x, se alinearán por la izquierda
■ Es necesario ralentizarla
● Indicarle que cambie el frame cuando transcurra un número determinado de llamadas a la función update
■ Es necesario ralentizarla
● Si el Sprite tiene n imágenes (frames) consecutivas y la transición entre ellas tiene que durar t segundos (𝑛𝑛⁄𝑡𝑡 𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖/𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠) cada imagen tiene que durar en
pantalla
○ 𝑡𝑡⁄
𝑛𝑛 𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠/𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖
○ 10 imágenes
■ Es necesario ralentizarla
○ 𝐹𝐹𝐹𝐹𝐹𝐹 𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢
�𝑠𝑠𝑠𝑠𝑠𝑠 × 𝑡𝑡⁄𝑛𝑛 𝑠𝑠𝑠𝑠𝑠𝑠
�𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖 =
(𝐹𝐹𝐹𝐹𝐹𝐹×𝑡𝑡)⁄ 𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢
𝑛𝑛 �𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖
(60×1) 𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢𝑢
○ �10 = 6 �𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖
● En consecuencia, habrá que cambiar la imagen cada 6 llamadas a la función update
○ Llevar un contador
● Además, las animaciones de los personajes suelen venir en la hoja dadas de un lado únicamente, debido
a que se pueden invertir (flip) respecto al eje que se desee.
● Para girar una imagen se puede usar un código similar a:
if self.mirando == IZQUIERDA:
pantalla.blit(
self.hoja.subsurface(
self.coordenadasHoja[self.numPostura][self.numImagenPostura]),
(self.posicionx, self.posiciony))
■ pygame.transform.flip
■ pygame.transform.scale
■ pygame.transform.rotate
■ pygame.transform.rotozoom
■ pygame.transform.scale2x
■ etc.
● ¿Cómo obtener las coordenadas de un frame en la hoja de Sprites y/o generar hojas de Sprites nuevas?
○ http://www.spritecow.com/
● Cada Sprite de un mismo tipo (jugadores, cada tipo de enemigo) deberá pertenecer a la misma clase.
○ Extender la clase Sprite
○ Incluso cuando el Sprite no haga nada, conviene extender esta clase y el método update.
● Cada Sprite puede pertenecer a un grupo, lo que facilita una división conceptual.
○ Por ejemplo:
■ Se puede tener un grupo de Sprites que sean Escenario, con Sprites tanto estáticos como dinámicos en él
■ También se puede tener otro grupo que fuera Enemigos, y otro llamado Personajes
■ Por ejemplo:
● Los Sprites en Enemigos, a su vez, podrían estar en distintos grupos según el tipo de enemigos que sean
○ add(*groups)
○ remove(*groups)
○ kill()
○ alive()
○ groups()
○ copy()
■ Duplica el grupo
○ add(*sprites)
○ remove(*sprites)
○ has(*sprites)
○ update(*args)
○ clear(dest,bg)
○ empty()
5 – con grupos.py
○ Elimina la necesidad de crear un array u otra estructura para mantener referencias a los mismos
● Una ventaja de utilizar grupos es que se pueden automatizar las llamadas a ciertas funciones
■ grupoJugadores.update()
○ Se pasan como parámetros a cada una de las llamadas a update de los miembros del grupo
■ Ejemplo:
■ Dice si el sprite está en colisión con algún Sprite del grupo grupoSprites y devuelve con cuál
■ Devuelve la lista de Sprites del grupo con los que está en colisión
■ dokill es un valor booleano que dice si los Sprites con los que está en colisión deben de ser eliminados del grupo
■ dokill1, dokill2 dicen si los Sprites en colisión se deben borrar de los grupos 1 y 2 respectivamente
● Un ejemplo de pasar un parámetro a la función update podría ser el siguiente, pasando el tiempo:
○ Sincronización por tiempo
def update(self,tiempo):
# Si vamos a la izquierda
posicionx y posiciony contienen
if self.movimiento == IZQUIERDA: los valores reales de la posición
# La postura actual sera estar caminando del Sprite (en punto flotante)
self.numPostura = ANDANDO
# Esta mirando a la izquierda
self.mirando = IZQUIERDA
# Actualizamos la posicion Sin embargo, el Sprite se dibujará
self.posicionx -= VELOCIDAD_JUGADOR * tiempo donde esté el atributo rect, así
self.rect.left = self.posicionx
# Actualizamos la imagen a mostrar que se sitúa rect en esa posición
self.actualizarPostura()
# Si vamos a la derecha En este caso, posicionx indica la
… coordenada del lado izquierdo del
rectángulo
● Además de rect.left, el rectángulo se podría situar de forma relativa a su margen derecho (rect.right) o
izquierdo (rect.left) en el eje x, y con respecto al superior (rect.top) o inferior (rect.bottom) en el eje y
○ Alinearlo con respecto a izq/dcha o arriba/abajo
○ Si se utiliza rect.bottom para situar el rectángulo, se soluciona el problema anterior de tener frames de distinta altura
■ Ejemplo: saltos
○ Solución de pygame
# Si estamos en el aire
if self.numPostura == SPRITE_SALTANDO:
# Actualizamos la posicion
self.posiciony -= self.velocidady * tiempo
# Si llegamos a la posicion inferior, paramos de caer
if (self.posiciony>300):
self.numPostura = SPRITE_QUIETO
self.posiciony = 300
self.velovidady = 0
# Si no, aplicamos el efecto de la gravedad
else:
self.velocidady -= 0.004
# Nos ponemos en esa posicion en el eje y
self.rect.bottom = self.posiciony
7 - con saltos.py
■ Utilidades en http://spritedatabase.net/download
○ http://spriters-resource.com/
● Al inicializar un Sprite
○ Cargar hoja de Sprites
● Gestores de recursos
○ En el código mostrado hasta ahora, cada vez que se crea un objeto se cargan sus datos (imagen, sonido, etc.) de disco
■ Un objeto que se encarga de gestionar la carga desde archivo de los recursos necesarios:
● Archivos de configuración
● Imágenes
● Sonidos
● Música
● Fuentes tipográficas
● Texturas
● Gestores de recursos
○ Cuando se necesita cargar un recurso de disco, en lugar de cargarlo directamente, se hace una llamada al gestor
○ Ahorro de tiempo
■ De esta manera, todos los objetos que han cargado ese recurso tienen una referencia al mismo objeto
● Ahorro de memoria
● Gestores de recursos
○ El gestor de recursos, por tanto, debe implementar un almacenamiento
■ Comparación de strings
● Ineficiente
● Gestores de recursos
○ Un gestor de recursos para un juego más complejo podría tener métodos especializados en cada tipo de recurso
■ Por ejemplo:
● CargarTextura()
● CargarSonido()
● CargarImagen()
● etc.
● Gestores de recursos
○ Un gestor de recursos genérico podría, además:
■ Implementar métodos para registrar las carpetas donde está cada recurso
● Gestores de recursos
○ Además, en un videojuego más complejo un gestor de recursos podría gestionar otros recursos más complejos
■ Por ejemplo:
● Escenas
● Modelos 3D
● Scripts
● Personajes, Sprites, …
● etc.
■ Crear «plantillas»
● Gestores de recursos
○ Implementación de un gestor de recursos:
■ Patrón Singleton
■ Métodos de clase
● Si se desea añadir interacción de los Sprites con el resto de los objetos del juego:
○ El resto de los objetos también son Sprites
■ Por ejemplo:
● Plataformas
● Enemigos
● Sprites dinámicos
● Items
● etc.
● Si se desea añadir interacción de los Sprites con el resto de los objetos del juego:
○ ¿Quién mantiene referencia a esos Sprites?
● Si se desea añadir interacción de los Sprites con el resto de los objetos del juego:
○ ¿Quién mantiene referencia a esos Sprites?
○ Sólo los Sprites muy importantes mantienen referencia por medio de atributos
■ fase.jugador1, fase.jugador2
● Si se desea añadir interacción de los Sprites con el resto de los objetos del juego:
○ ¿Quién mantiene referencia a esos Sprites?
● Ejemplos de grupos:
○ GruposSpritesDinamicos
○ GrupoSprites
3 – Juego/fase.py
…
def __init__(self):
self.jugador1 = Jugador()
self.jugador2 = Jugador()
self.grupoJugadores = pygame.sprite.Group( self.jugador1, self.jugador2 )
3 – Juego/fase.py
● Instanciador de atributos
● Cuando los jugadores tienen atributos que van a conservar su valor de una fase a otra: número de vidas, energía, etc.
● Se crearían antes de crear el objeto Fase, en el código principal, y se pasarían de una fase a otra
○ El objeto Fase recibe los objetos Jugador, crea los Sprites y los asigna
● Se descartarían o no
■ Orden de actualización
● Llamada a update
■ Orden de dibujo:
■ etc.
○ Desde este método se dibujará el fondo y se llamará al método draw de los grupos
escena.procesarEventos(eventos
)
escena.update(tiempo)
escena.dibujar(pantalla)
# Actualiza la escena
# Devuelve si se debe parar o no el juego Se llama al método
if (fase.update(tiempo_pasado)): update del objeto Fase
pygame.quit()
sys.exit()
Se llama al método dibujar
# Se dibuja en pantalla
del objeto Fase
fase.dibujar(pantalla)
pygame.display.flip() El volcado de la pantalla se hace en el bucle
3 – Juego/main.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
● Transparencia anterior
■ Orden de pintado:
● Podría hacerse
■ Para, por ejemplo, pintar los jugadores por encima del resto de Sprites
■ Las acciones a realizar por los jugadores ya fueron indicadas en los eventos
■ El método update comprueba que los movimientos que se quiere hacer son correctos (no intersecan con paredes,
por ejemplo), y los realiza
■ El método update de Jugador comprobará si hay colisión con algún enemigo, y bajará el nivel de vida
■ O si su vida llega a 0
class Fase:
def __init__(self):
self.rect = self.imagen.get_rect()
self.rect.bottom = ALTO_PANTALLA Se guarda el nivel en el que está
el scroll horizontal (inicialmente a
# La subimagen que estamos viendo 0)
self.rectSubimagen = pygame.Rect(0, 0, ANCHO_PANTALLA, ALTO_PANTALLA)
self.rectSubimagen.left = 0
# El scroll horizontal empieza en la posicion 0 por defecto
def dibujar(self,pantalla):
# Dibujamos el color del cielo
pantalla.fill(self.colorCielo)
# Y ponemos el sol
pantalla.blit(self.sol, self.rect) 3 – Juego/fase.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
■ Una vez movidos los Sprites, y según la posición de los jugadores, puede haber que cambiar
el scroll
● Si hay que cambiarlo, actualizar la posición de todos los Sprites
○ Estáticos y dinámicos
■ No se actualiza el scroll
○ No se actualiza el scroll
● Habría que desplazar el jugador izquierdo, el decorado y todos los sprites desplazamiento
pixeles a la derecha
● Pero puede ocurrir que el jugador de la derecha se encuentre a una distancia del borde derecho
menor que desplazamiento píxeles
● No se actualiza el scroll
■ Estáticos y dinámicos
# Si ambos jugadores se han ido por ambos lados de los dos bordes
if (jugadorIzq.rect.left<MINIMO_X_JUGADOR) and
Si ambos jugadores se han
(jugadorDcha.rect.right>MAXIMO_X_JUGADOR):
ido por ambos lados, se
coloca uno en cada lado.
# Colocamos al jugador que esté a la izq a la izq de todo
Devuelve False: no se ha
jugadorIzq.establecerPosicion((self.scrollx+MINIMO_X_JUGADOR,
actualizado
jugadorIzq.posicion[1]))
# Colocamos al jugador que esté a la derecha a la derecha de todo
jugadorDcha.establecerPosicion((self.scrollx+MAXIMO_X_JUGADOR-
jugadorDcha.rect.width, jugadorDcha.posicion[1]))
… 3 – Juego/fase.py
# Si ambos jugadores están entre los dos límites de la pantalla, no se hace nada
return False;
3 – Juego/fase.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
Parte que se ve en
pantalla
Sprite
MiSprite
■ Contiene los atributos y métodos para situar los Sprites de forma global (en el entorno) o local (en pantalla)
○ pygame.Rect
● Scroll actual
○ Almacena el valor de scroll para, cuando se modifique su posición global, también se modifique su posición en la pantalla
■ Importante:
● Implementar esta clase sólo si se va a realizar scroll
● establecerPosicion( nuevaPosicionGlobal )
○ Recibe una nueva posición global y cambia su posición de forma global
■ También, gracias al valor de scroll que tiene almacenado, su posición en pantalla
● establecerPosicionPantalla( nuevoScroll )
○ Recibe un valor de scroll y cambia su valor en pantalla
■ Pero no su posición global en el entorno
● Un atributo de velocidad
○ Recibe el tiempo
○ Con el atributo de velocidad, calcula la distancia recorrida y la actualiza con el método establecerPosicion() anterior
■ De esta manera, mover un Sprite sólo implica establecer sus valores de velocidad y llamar a update
● Si se extiende MiSprite y se implementa update, se puede llamar a update de MiSprite después de establecer la velocidad
■ Esto permite, además, implementar de una forma sencilla distintos efectos físicos como
● Inercia
● Aceleración / deceleración
● Gravedad
● etc.
3 – Juego/personaje.py
■ Cuando se actualiza el scroll en función de las posiciones de los jugadores, se modifica la posición de todos los Sprites
○ Jugadores
○ Enemigos
○ Disparos
○ Items
○ Plataformas
○ etc.
○ Implementación:
■ Ejemplo anterior:
● posicion = (190, y)
■ Implementación:
● Distintos objetos Decorado, cada uno con una línea distinta en update:
class Decorado:
…
def update(self, scrollx):
self.rectSubimagen.left = scrollx 3 – Juego/fase.py
■ Problema: al escalar una imagen de poca resolución a más resolución, aparece pixelada
■ Guardar distintas versiones de cada imagen (decorados, Sprites, etc.), una para cada resolución distinta
○ Solución:
● “metros” o “metros/seg”
● “metros” → píxeles
● Resolución
● Volumen de sonido
● Corrección gamma
● etc.
■ Implementa métodos:
● Cuando se quiere cargar un archivo de imagen (a través del gestor de recursos), completa el nombre del archivo
○ Ejemplo:
■ Configuracion.Instancia().nombreArchivoImagen(‘decorado’)
● Devuelve ‘decorado1280x800.jpeg’
● etc.
● Introduciendo plataformas:
○ Clase plataforma: subclase de Sprite
● Una
class plataforma sólo necesita existir
Plataforma(MiSprite):
def __init__(self,rectangulo):
# Primero invocamos al constructor de la clase padre
MiSprite.__init__(self)
# Rectangulo con las coordenadas en pantalla que ocupara
self.rect = rectangulo
# Y lo situamos de forma global en esas coordenadas En este caso, las
self.establecerPosicion((self.rect.left, self.rect.bottom)) plataformas no
# En el caso particular de este juego, las plataformas no se van a ver, tienen imagen
asi que no se carga ninguna imagen
self.image = pygame.Surface((0, 0))
3 – Juego/fase.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
● Introduciendo plataformas:
○ Las plataformas pertenecen al objeto Fase
■ La fase sabe en qué lugares están, las crea con sus posiciones
class Fase:
def __init__(self):
…
# Creamos las plataformas del decorado
# La plataforma que conforma todo el suelo
plataformaSuelo = Plataforma(pygame.Rect(0, 550, 1200, 15))
# La plataforma del techo del edificio
plataformaCasa = Plataforma(pygame.Rect(870, 417, 200, 10))
# y el grupo con las mismas
self.grupoPlataformas = pygame.sprite.Group( plataformaSuelo, plataformaCasa )
# Estas plataformas deberían de cargarse de un archivo, puesto que deberian
# de ser distintas para cada decorado
3 – Juego/fase.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
● Introduciendo plataformas:
○ En el ejemplo, la fase crea las plataformas en las siguientes posiciones:
■ Suelo
● Introduciendo plataformas:
○ Observaciones (1/5):
● En un caso real:
● Introduciendo plataformas:
○ Observaciones (2/5):
○ 2 opciones (1/2):
○ etc.
● Introduciendo plataformas:
○ Observaciones (3/5):
○ 2 opciones (2/2):
● Cada fase, en el constructor, instancia las plataformas, enemigos, etc. en las posiciones que corresponda y el decorado de esa fase
Fase
Fase1 Fase2
● Introduciendo plataformas:
○ Observaciones (4/5):
○ 2 opciones (2/2):
FaseScrollHorizontal FaseAérea
En el constructor se indica las
características de cada fase
Fase1 Fase2
● Introduciendo plataformas:
○ Observaciones (5/5):
○ Surface(0,0)
● Al llamar a draw del grupo, sólo se dibujarán las que tienen imagen
● Introduciendo plataformas:
○ Para comprobar cuándo un jugador “cae” en una plataforma, habrá que comprobar si hay colisión entre el jugador y las
plataformas del grupo
● Dice si el sprite está en colisión con algún Sprite del grupo grupoSprites y devuelve con cuál
■ En el caso de este juego sólo nos interesan las colisiones que se producen cuando el personaje está en caída
● Introduciendo plataformas
○ Además, un personaje que no esté en el aire debe de estar en colisión con una plataforma
■ En caso contrario, en la próxima llamada al método update, este deberá detectarlo y poner al personaje cayendo
■ Cuando un personaje está cayendo y se detecta colisión con una plataforma, se detiene la caída y se sitúa al personaje encima
de la plataforma
● Pero con una línea de píxeles de colisión, para que en la próxima llamada se detecte que está por encima
# Miramos a ver si hay que parar de caer: si hemos llegado a una plataforma
# Para ello, miramos si hay colision con alguna plataforma del grupo
plataforma = pygame.sprite.spritecollideany(self, grupoPlataformas)
# Ademas, esa colision solo nos interesa cuando estamos cayendo
# y solo es efectiva cuando caemos encima, no de lado, es decir,
# cuando la posicion inferior esta por encima de la parte de abajo de la plat.
if (plataforma != None) and (velocidady>0) and
(plataforma.rect.bottom>self.rect.bottom):
# Lo situamos con la parte de abajo un pixel colisionando con la plataforma
# para poder detectar cuando se cae de ella
self.establecerPosicion(
(self.posicion[0], plataforma.posicion[1]-plataforma.rect.height+1))
# Lo ponemos como quieto
self.numPostura = SPRITE_QUIETO
# Y estará quieto en el eje y
velocidady = 0
● Si se desea añadir interacción de los Sprites con el resto de los objetos del juego:
○ El resto de los objetos también son Sprites
■ Por ejemplo:
● Plataformas MiSprite
● Enemigos
● Items
Personaje
Jugador NoJugador
● Introduciendo enemigos:
○ Clase “MiSprite”
■ Subclase de Sprite
■ Contiene los atributos con la localización de un Sprite en el entorno del videojuego y en pantalla
● Cambiar de posición
○ Se modifica el scroll
■ Contiene atributos de velocidad que permiten modificar posiciones de una manera más sencilla
● Introduciendo enemigos:
○ Clase “Personaje”
■ Subclase de MiSprite
■ Constructor:
● Movimiento
● etc.
● Introduciendo enemigos:
○ Clase “Jugador”
■ Subclase de “Personaje”
■ Constructor:
● Llama al constructor de Personaje con las características propias del jugador: hoja de sprites, velocidad, etc.
■ Método update
● Utiliza el de “Personaje”
■ Método mover
○ Recibe el array de teclas pulsadas y qué tecla (constante) es de cada movimiento en este objeto
def __init__(self):
# Invocamos al constructor de la clase padre con la configuracion
# de este personaje concreto
Personaje.__init__(self,'Jugador.png','coordJugador.txt', [6, 12, 6],
VELOCIDAD_JUGADOR, VELOCIDAD_SALTO_JUGADOR, RETARDO_ANIMACION_JUGADOR);
Recibe el array con las teclas
def mover(self, teclasPulsadas, arriba, abajo, izquierda, derecha): pulsadas y qué teclas
# Indicamos la acción a realizar segun la tecla pulsada para el jugador utilizará este objeto
if teclasPulsadas[arriba]:
Personaje.mover(self,ARRIBA)
elif teclasPulsadas[izquierda]:
Personaje.mover(self,IZQUIERDA)
elif teclasPulsadas[derecha]:
Personaje.mover(self,DERECHA)
else:
Personaje.mover(self,QUIETO)
3 – Juego/personajes.py
■ Implementa métodos que devuelven valor booleano según las acciones del jugador
● Estos métodos reciben los eventos y/o la lista de teclas/botones pulsados y dicen si se quiso realizar una acción determinada
■ Clase abstracta
■ Realiza llamadas a cada método de acción de su objeto Control con esos datos como parámetros
● Introduciendo enemigos:
○ Clase “NoJugador”
■ Subclase de “Personaje”
■ Clase abstracta para todos los posibles personajes que no son controlados por el jugador
■ Método mover_cpu
● Sin definir
○ En función de la IA, la función debería realizar una u otra determinada llamada a mover (superclase “Personaje”)
class NoJugador(Personaje):
● Introduciendo enemigos:
○ Clase “Sniper”
■ Constructor:
● Llama al constructor de Personaje con las características propias del jugador: hoja de sprites, velocidad, etc.
■ Método mover_cpu:
● IA rudimentaria:
class Sniper(NoJugador):
"El enemigo 'Sniper'"
def __init__(self):
# Invocamos al constructor de la clase padre con la configuracion de este personaje
NoJugador.__init__(self,'Sniper.png','coordSniper.txt', [5, 10, 6],
VELOCIDAD_SNIPER, VELOCIDAD_SALTO_SNIPER, RETARDO_ANIMACION_SNIPER);
Implementación de la
IA de este personaje
# Aqui vendria la implementacion de la IA segun las posiciones de los jugadores concreto
# La implementacion de la inteligencia segun este personaje particular
def mover_cpu(self, jugador1, jugador2):
● Además, el objeto de la clase Fase ha de comprobar si hay colisión entre los jugadores y los enemigos
○ Método update
# Actualizamos el scroll
self.actualizarScroll(self.jugador1, self.jugador2)
# Actualizamos el fondo: la posicion del sol y el color del cielo
self.fondo.update(tiempo)
# No se debe parar la ejecucion
return False
3 – Juego/fase.py
Contornos Inmersivos, Interactivos y de Entretenimiento Enrique Fernández-Blanco
Programación en 2D
● Fondo
● Decorado
● Plataformas
● Sprites
● Proyectiles
● Control HUD
■ Clase Capa
■ El método dibujar() de Fase va llamando sucesivamente a los métodos dibujar() de cada objeto Capa en el orden adecuado
■ Método utilizado, sobre todo, si en el juego existen distintos niveles de profundidad en la pantalla
● Mapeado:
○ Creación de mapas aquí descrita de forma “a bajo nivel”
● Mapeado:
○ Para crear una máscara a partir de una imagen:
■ pygame.mask.from_surface
■ pygame.mask.from_threshold
○ Una vez se tienen dos máscaras (mapa y personaje), para comprobar que un personaje puede ir por cierta parte del mapa:
■ mascaraMapa.overlap_area(mascaraPersonaje, desplazamiento)
● Mapeado:
○ Editores de mapas:
■ GLEED2D
● http://www.gleed2d.de/
● http://www.dannylum.com/D2DProject/index.html
● http://www.tilemap.co.uk/mappy.php
■ Tile Mapper
● http://tilemapeditor.com/
● Mapeado:
○ Editores de mapas:
■ La mayoría de ellos no están pensados para su uso con Python, pero para cargarlos con Python:
○ http://silveiraneto.net/2009/12/19/tiled-tmx-map-loader-for-pygame/
● Mappy Exporter
○ http://trac2.assembla.com/trollshit/browser
class Player(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image=img_player
self.rect=self.image.get_rect()
self.mask=pygame.mask.from_surface(self.image)
○ Si se van a calcular muchas colisiones mediante esta función, calcular el atributo mask para no tener que calcular la
máscara para cada colisión
● Esta función no está pensada para ser usada sola, sino en combinación con alguna otra de colisión
● Otras opciones:
■ Las funciones spritecollide, groupcollide y
spritecollideany aceptan un parámetro opcional que es la ○ collide_rect
función de colisión
■ Comprueba con el rectángulo
● Si no se especifica, usa por defecto la función ○ collide_rect_ratio
collide_rect()
■ Comprueba con el rectángulo escalado
○ Comprueba colisión de dos Sprites según sus
atributos rect ○ collide_circle
○ collide_circle_ratio
○ collide_mask
● Escenas:
○ Una escena se define como cada una de las etapas que posee el juego global
○ Un videojuego puede tener muchas escenas, pero sólo una está activa en un momento determinado
○ Por ejemplo:
■ La pantalla inicial
■ La pantalla de opciones
● Escenas:
○ Por ejemplo, un juego con estas escenas:
■ Pero existe un hilo conductor que contiene la lógica que conecta las escenas
● Escenas:
○ El componente que se ocupa de gestionar la ejecución de cada escena se denomina director
○ El director conoce qué escena está activa, y mantiene una pila de escenas para permitir acciones como “llamadas entre escenas”
■ Por ejemplo: pausando una escena, entrando en la escena de pausa, y más tarde, volviendo a la original
● Escenas:
○ Cada escena conoce su lógica interna
● etc.
■ Además, cada escena sabe cuándo tiene que terminar y empezar otra escena
● Escenas:
○ No confundir escena con pantalla
● Por ejemplo:
○ Aventura gráfica
○ Menú de inicio
● Escenas:
○ El director implementa el bucle de eventos
def __init__(self):
# Pila de escenas
self.pila = []
Implementa una pila de escenas
# Flag que nos indica cuando quieren salir de la escena
self.salir_escena = False
# Reloj
self.reloj = pygame.time.Clock()
Flag para que una escena pueda indicar que se quiere terminar. Si se
El director posee el objeto reloj pone a True, la escena no terminará inmediatamente, sino cuando acabe
esa iteración del bucle de eventos
4 – Escenas/director.py
# El bucle del juego, las acciones que se realicen se harán en cada escena
while not self.salir_escena:
El director
# Sincronizar el juego a 60 fps
tiempo_pasado = self.reloj.tick(60) implementa
el bucle del
# Pasamos los eventos a la escena
juego
escena.eventos(pygame.event.get())
# Actualiza la escena
escena.update(tiempo_pasado)
# Se dibuja en pantalla
escena.dibujar(self.screen)
pygame.display.flip()
4 – Escenas/director.py
# El bucle del juego, las acciones que se realicen se harán en cada escena
while not self.salir_escena:
Envía la lista de
El director
# Sincronizar el juego a 60 fps eventos a la
tiempo_pasado = self.reloj.tick(60) implementa
escena
Actualiza la escena: el bucle del
# Pasamos los eventos a la escena juego
escena.eventos(pygame.event.get()) llama al método
update de la escena
# Actualiza la escena
escena.update(tiempo_pasado)
Llama al método de dibujar de
# Se dibuja en pantalla la escena, pero es el director
escena.dibujar(self.screen)
pygame.display.flip() el que cambia el buffer
4 – Escenas/director.py
● Escenas:
○ El director implementa una pila de escenas:
● o apilar la nueva
● En este caso, el director la eliminará de la pila y ejecutará la siguiente que esté en la cima
● Escenas:
○ El director implementa métodos:
● Escenas:
○ El director implementa métodos:
def ejecutar(self):
Mientras haya
escenas en la pila, se
# Mientras haya escenas en la pila, ejecutaremos la de arriba ejecutará la de la
while (len(self.pila)>0):
cima
# Se coge la escena a ejecutar como la que este en la cima de la pila
escena = self.pila[len(self.pila)-1] Se toma la escena
de la cima
# Ejecutamos el bucle de eventos hasta que termine la escena
self.bucle(escena)
Se ejecuta el bucle
en esta escena,
hasta que ella
decida terminar
Ejecutar la pila de escenas. Inicialmente sólo habrá una
4 – Escenas/director.py
Director Escena
Menu Fase
4 – Escenas/escena.py
class Escena:
Mantiene una referencia al director para
def __init__(self, director): poder llamar a métodos como salirEscena
self.director = director o salirPrograma
def update(self, *args):
raise NotImplemented("Tiene que implementar el metodo update.")
■ Método update:
def update(self, tiempo):
self.grupoSpritesDinamicos.update(self.grupoPlataformas, tiempo)
if pygame.sprite.groupcollide(
self.grupoJugadores, self.grupoEnemigos, False, False)!={}:
self.director.salirEscena()
self.actualizarScroll(self.jugador1, self.jugador2)
self.fondo.update(tiempo)
■ Método eventos:
4 – Escenas/fase.py
def eventos(self, lista_eventos):
# Miramos a ver si hay algun evento de salir del programa
for evento in lista_eventos:
# Si se quiere salir, se le indica al director
if evento.type == pygame.QUIT:
self.director.salirPrograma()
● Escena Menu:
○ Contiene el menú inicial
● Escena Menu:
○ Cada pantalla contiene los elementos de la interfaz
■ El objeto Pantalla envía el evento a cada uno de los objetos ElementoGUI, que le devuelven True o False si el clic fue en él
○ etc.
● Escena Menu:
Escena
4 – Escenas/menu.py
● Escena Menu:
○ Clase ElementoGUI:
class ElementoGUI:
def __init__(self, pantalla, rectangulo): Guarda una referencia a la pantalla a la que
self.pantalla = pantalla pertenece, y el rectángulo que ocupa en
self.rect = rectangulo pantalla, para saber si se ha hecho clic
def establecerPosicion(self, posicion):
(posicionx, posiciony) = posicion Método para situarlo en pantalla
self.rect.left = posicionx
self.rect.bottom = posiciony
def posicionEnElemento(self, posicion): Método que
(posicionx, posiciony) = posicion
if (posicionx>=self.rect.left) and (posicionx<=self.rect.right) and dice si se ha
(posiciony>=self.rect.top) and (posiciony<=self.rect.bottom): hecho clic en
return True él
else:
return False
def dibujar(self):
Método
raise NotImplemented("Tiene que implementar el metodo dibujar.")
def accion(self): abstractos
raise NotImplemented("Tiene que implementar el metodo accion.")
4 – Escenas/menu.py
● Escena Menu:
○ Clases Boton, BotonJugar y BotonSalir:
class Boton(ElementoGUI):
def __init__(self, pantalla, nombreImagen, posicion):
# Se carga la imagen del boton Constructor recibe
self.imagen = GestorRecursos.CargarImagen(nombreImagen,-1)
self.imagen = pygame.transform.scale(self.imagen, (20, 20)) la imagen del
# Se llama al método de la clase padre con el rectángulo botón y la posición
ElementoGUI.__init__(self, pantalla, self.imagen.get_rect())
# Se coloca el rectangulo en su posicion
self.establecerPosicion(posicion)
def dibujar(self, pantalla):
pantalla.blit(self.imagen, self.rect)
Método para dibujarlo en pantalla
class BotonJugar(Boton):
def __init__(self, pantalla):
Boton.__init__(self, pantalla, 'boton_verde.png', (580,530))
def accion(self): Clases hijas
self.pantalla.menu.ejecutarJuego() implementan el
class BotonSalir(Boton):
def __init__(self, pantalla):
método accion()
Boton.__init__(self, pantalla, 'boton_rojo.png', (580,560))
def accion(self):
self.pantalla.menu.salirPrograma()
4 – Escenas/menu.py
● Escena Menu:
○ Clases TextoGUI y TextoJugar:
class TextoGUI(ElementoGUI):
def __init__(self, pantalla, fuente, color, texto, posicion):
# Se crea la imagen del texto Constructor
self.imagen = fuente.render(texto, True, color)
# Se llama al método de la clase padre con el rectángulo recibe el texto, la
ElementoGUI.__init__(self, pantalla, self.imagen.get_rect()) fuente, el color y
# Se coloca el rectangulo en su posicion la posición
self.establecerPosicion(posicion)
class PantallaGUI:
def __init__(self, menu, nombreImagen):
self.menu = menu
# Se carga la imagen de fondo
self.imagen = GestorRecursos.CargarImagen(nombreImagen)
self.imagen = pygame.transform.scale(self.imagen, (ANCHO_PANTALLA, ALTO_PANTALLA))
# Se tiene una lista de elementos GUI
self.elementosGUI = []
● Escena Menu:
○ Clase Menu (1/3):
class Menu(Escena):
● Escena Menu:
○ Clase Menu (2/3):
#--------------------------------------
# Metodos propios del menu
4 – Escenas/menu.py
● Código principal:
○ Crear el director
4 – Escenas/main.py
○ 2 posibilidades:
■ Se desea ir a un menú
■ Si se para, en la siguiente iteración el tiempo transcurrido en la iteración anterior (durante la pausa) será demasiado alto
● Al calcular las nuevas posiciones de los Sprites según su velocidad, estas no serán correctas
if (not self.pausa):
self.grupoSpritesDinamicos.update(self.grupoPlataformas, tiempo)
…
■ El bucle se sigue ejecutando, pero no se actualizan los objetos
○ Proceso similar si se quiere congelar el juego para mostrar un mensaje por pantalla, etc.
● Se desea ir a un menú
○ Este menú será una nueva escena (MenuPausa)
○ El director, cuando acabe el ciclo actual, pasará a ejecutar la escena que está en la cima de la pila (MenuPausa)
■ Cuando termine, la sacará de la pila y volverá a la escena que está en la cima (Fase) en el punto en el que la dejó
● Director (resumen):
○ Implementa el bucle de eventos
● apilarEscena
● cambiarEscena
● salirEscena
● salirPrograma
● Otras funcionalidades:
○ Clipping
● El resto puede estar ocupada por elementos como menú de estado, panel, etc.
■ Podría ocurrir que estas posiciones coincidiesen encima del estos paneles
● Otras funcionalidades:
○ Clipping
■ Ejemplo:
● Animaciones:
○ Sprites
● Necesario calcular cada cuántas veces hay que cambiar la imagen a mostrar
○ Mejor técnica:
● Definirla a partir de las imágenes que componen la animación y el tiempo que durará cada una
● Animaciones: Imagen Tiempo (seg.) Imagen Tiempo (seg.) Imagen Tiempo (seg.)
○ Ejemplo:
0.1
0.1 0.1
0.1
0.1 0.1
0.1
0.1 0.2
0.1
0.1 0.2
0.1
0.1 0.2
0.1
● Animaciones:
○ Un sprite “clásico” también se puede ver como una animación
● Animaciones:
○ De igual manera, una animación puede verse como un sprite
Imagen Tiempo (seg.)
■ Calcular, en cada llamada a update, según el tiempo transcurrido, si es necesario cambiar
de imagen o no
0.1
● Según la duración de cada imagen y el FPS, habrá que calcular cada cuántas
llamadas a update hay que cambiar la imagen
● Ejemplo: como si estas imágenes estuvieran montadas sobre una hoja de sprites 0.1
0.1
0.1
0.1
0.1
● Animaciones:
○ ¿Cuándo utilizar un tipo u otro?
■ Animaciones
○ Como un vídeo
■ Pausar
■ Rebobinar
■ etc.
○ y otras como
■ Cambiar de tamaño
■ Rotar
■ etc.
● Animaciones:
○ ¿Cómo mostrar animaciones con pygame?
■ http://inventwithpython.com/pyganim/
● Animaciones:
○ Clase PygAnimation:
Constructor: se le indican las imágenes y
class PygAnimation(object): la duración de cada una (frames)
def __init__(self, frames, loop=True)
● Ejemplo:
○ Se puede extender esa clase para darle atributos de posición
# Extendemos la clase animacion de PygAnimation para darle posicion
class Animacion(pyganim.PygAnimation):
def __init__(self, *args):
pyganim.PygAnimation.__init__(self, args)
# Posicion que tendra esta animacion
self.posicionx = 0
self.posiciony = 0
● Ejemplo:
○ Cada una de las animaciones que haya pueden ser
class AnimacionFuego(Animacion):
def __init__(self):
pyganim.PygAnimation.__init__(self,[
('imagenes/flame_a_0001.png', 0.1),
('imagenes/flame_a_0002.png', 0.1),
('imagenes/flame_a_0003.png', 0.1),
('imagenes/flame_a_0004.png', 0.1),
('imagenes/flame_a_0005.png', 0.1),
('imagenes/flame_a_0006.png', 0.1)])
● Ejemplo:
○ Cada una de las animaciones que haya pueden ser
class AnimacionRayo(Animacion):
def __init__(self):
pyganim.PygAnimation.__init__(self,[
('imagenes/bolt_strike_0001.png', 0.1),
('imagenes/bolt_strike_0002.png', 0.1),
('imagenes/bolt_strike_0003.png', 0.1),
('imagenes/bolt_strike_0004.png', 0.1),
('imagenes/bolt_strike_0005.png', 0.1),
('imagenes/bolt_strike_0006.png', 0.1),
('imagenes/bolt_strike_0007.png', 0.1),
('imagenes/bolt_strike_0008.png', 0.1),
('imagenes/bolt_strike_0009.png', 0.1),
('imagenes/bolt_strike_0010.png', 0.1)])
● Ejemplo:
○ Cada una de las animaciones que haya pueden ser
● Ejemplo:
○ Animaciones en la escena Menu:
■ Modificar el constructor de la clase abstracta PantallaGUI para que almacene una lista de animaciones
■ Modificar el método dibujar de la clase abstracta PantallaGUI para que dibuje también las animaciones
■ Cada clase hija de PantallaGUI instanciará en el constructor las animaciones que use y las dará de alta en la lista
class PantallaGUI:
def __init__(self, menu, nombreImagen):
self.menu = menu
# Se carga la imagen de fondo
self.imagen = GestorRecursos.CargarImagen(nombreImagen)
self.imagen = pygame.transform.scale(self.imagen, (ANCHO_PANTALLA, ALTO_PANTALLA))
# Se tiene una lista de elementos GUI
self.elementosGUI = []
# Se tiene una lista de animaciones Se crea la lista de animaciones
self.animaciones = []
class PantallaInicialGUI(PantallaGUI):
def __init__(self, menu):
PantallaGUI.__init__(self, menu, 'portada.jpg')
# Creamos los botones y los metemos en la lista
…
# La animacion del fuego
animacionFuego = AnimacionFuego()
Se crea la animación de la
# Aumentamos un poco el tamaño de la animacion escena
animacionFuego.scale((200,200)) Se transforma la animación
# La situamos en su posicion
animacionFuego.posicionx = 70 Se sitúa en el sitio donde estará
animacionFuego.posiciony = 100
# Iniciamos la animacion
animacionFuego.play() Se empieza a reproducir
# Y la introducimos en la lista
self.animaciones.append(animacionFuego) Se introduce en la lista
● Ejemplo:
○ Escena Fase:
■ Constructor:
class Fase(Escena):
def __init__(self, director):
● Ejemplo:
○ Escena Fase:
■ Constructor:
self.animacionesDetras = []
for i in range(9):
# La animacion del fuego
animacionFuego = AnimacionFuego()
# Aumentamos un poco el tamaño de la animacion Se crean las
animacionFuego.scale((400,400)) animaciones que
# La situamos en su posicion
estarán “detrás”
animacionFuego.posicionx = 120*i - 200
animacionFuego.posiciony = 250 del decorado y
# Iniciamos la animacion se meten en una
animacionFuego.play() lista
animacionFuego.nextFrame(i)
# y la anadimos a la lista de animaciones detras
self.animacionesDetras.append(animacionFuego)
5 – Animaciones con pygame/fase.py
● Ejemplo:
○ Escena Fase:
■ Constructor:
self.animacionesDelante = []
for i in range(11):
# La animacion del fuego
animacionFuego = AnimacionFuego()
# Aumentamos un poco el tamaño de la animacion Se crean las
animacionFuego.scale((450,450)) animaciones que
# La situamos en su posicion
estarán “delante”
animacionFuego.posicionx = 120*i - 200
animacionFuego.posiciony = 450 del decorado y
# Iniciamos la animacion se meten en una
animacionFuego.play() lista
animacionFuego.nextFrame(i)
# y la anadimos a la lista de animaciones delante
self.animacionesDelante.append(animacionFuego)
5 – Animaciones con pygame/fase.py
● Ejemplo:
○ Escena Fase:
■ Método dibujar:
● Con esta forma de realizar animaciones se puede controlar el tiempo de cada frame, y de la animación en
general:
○ Útil para introducir animaciones dentro de un juego desarrollado con pygame
○ Pero para realizar una escena que sea una animación se necesitan otras funciones
■ Librería pyglet
○ No está orientada al desarrollo de videojuegos, aunque puede ser utilizada para ello
■ Algunas opciones, como la detección de colisiones, no las realiza, hay que gestionarlas a mano