Interfaz SDL
Tabla de Contenidos
Genie también se puede ejecutar con SDL (Simple DirectMedia Layer), un conocido motor de juegos en 2 dimensiones que proporciona herramientas para el desarrollo de videojuegos y aplicaciones multimedia. Así, se utiliza SDL en software de reproducción de vídeo, emuladores y muchos videojuegos.
SDL es un conjunto de bibliotecas desarrolladas en el lenguaje de programación C que proporcionan funciones básicas para realizar operaciones de dibujo en dos dimensiones, gestión de efectos de sonido y música, además de carga y gestión de imágenes. La biblioteca se distribuye bajo licencia LGPL aunque a partir de la versión 2.0 (actualmente en la 2.0.5, también disponible para Genie) se encuentra bajo la licencia de software libre ZLib que permite usar SDL libremente en cualquier software.
Se trata de una biblioteca multiplataforma (con soporte oficial para Windows, Mac OS X, Linux, iOS y Android, y para otras plataformas en el código fuente) diseñada para proporcionar acceso a audio, teclado, ratón, joystick y hardware gráfico a través de OpenGL y Direct3D.
Su uso requiere tener instaladas las librerias SDL que son básicamente libsdl1.2-dev y libsdl-gfx1.2-5 (a veces con otros nombres, como lib32-sdl o sdl2_gfx), aunque habitualmente las aplicaciones recurren a alguna más, como libsdl-image1.2, libsdl-mixer1.2, libsdl-sound1.2, libsdl-ttf2.0-0. Y las nuevas versiones: libsdl2-dev, libsdl2-gfx-1.0-0, libsdl2-image-2.0-0, libsdl2-mixer-2.0-0… Todas ellas disponibles en los repositorios oficiales de la mayoría de distribuciones linux (en Debian y derivadas como Ubuntu tan fácil como 'sudo apt-get install libsdl2-2.0' y 'sudo apt-get install libsdl2-dev', aunque se sigue usando libsdl1.2debian y libsdl1.2-dev, por lo que también serán necesarias). Para otros sistemas operativos ver Installing SDL.
A la hora de compilar, igual que con otras librerías, necesitamos especificar estos parámetros a valac:
valac --pkg sdl --pkg sdl-gfx nombre_archivo.gs
En función de que la aplicación utilice otras librerías SDL habrá que indicarlo al compilador, por ejemplo:
valac --pkg sdl --pkg sdl-gfx --pkg sdl-image --pkg sdl-mixer nombre_archivo.gs
Podemos probar que estamos preparados con un simple código de prueba:
// compila con valac --pkg sdl nombre_archivo.gs uses SDL init if (SDL.init(SDL.InitFlag.VIDEO) != 0 ) stderr.printf("Error: %s\n", SDL.get_error() ) SDL.quit() print "Test superado"
Crear una ventana
// compila con valac --pkg sdl nombre_archivo.gs uses SDL init // inicializamos SDL SDL.init(SDL.InitFlag.VIDEO) // ponemos título a la barra superior // set_caption (string title, string icon) SDL.WindowManager.set_caption ("Genie SDL Demo","") // creamos la ventana y elegimos el modo de video // set_video_mode (int width, int height, int bpp (bit por pixel), uint32 flags) screen: unowned SDL.Screen screen = SDL.Screen.set_video_mode (640, 480, 30, 0) // bucle principal: verifica eventos y actualiza la pantalla gameover: bool = false while (!gameover) // creamos un evento evento: SDL.Event while (Event.poll (out evento)) == 1 // termina al cerrar la ventana if evento.type ==SDL.EventType.QUIT gameover = true break // termina al pulsar ESC else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break screen.flip () // cierra SDL SDL.quit ()
Los comentarios del código aclaran bastante cada paso:
✔ Inicializamos el sistema de video SDL con SDL.init(SDL.InitFlag.VIDEO).
✔ Ponemos un título a la ventana de la aplicación.
✔ Creamos la ventana. El modo de video admite estos parámetros (por este orden):
- ancho,
- alto,
- bits por pixel, y
- flags. SDL.SetVideoMode devuelve el framebuffer de vídeo como SDL.Surface que se refiere a una estructura de superficie gráfica que representan áreas de memoria gráfica. Se expresa así, por ejemplo: SurfaceFlag.FULLSCREEN. Los valores (flags) que admite Surface son:
- SWSURFACE. Se almacena en la memoria del sistema.
- HWSURFACE. Se almacena en la memoria de vídeo.
- ASYNCBLIT. Habilita el modo de visualización asíncrono, puede acelerar en algunos CPU.
- ANYFORMAT. Si no se puede usar el valor bpp solicitado, SDL genera una pantalla de vídeo apropiada.
- HWPALETTE. Tiene una paleta exclusiva.
- DOUBLEBUF. Habilita el búfer doble, la mayoría se utilizará con el indicador HWSURFACE
- FULLSCREEN. Pantalla completa (Superficie de visualización).
- OPENGL. Contexto de renderizado OpenGL.
- OPENGLBLIT. Igual que el anterior, pero usa operaciones normales de blitting (blitting = mostrando superficies).
- RESIZABLE. Es redimensionable (Superficie de visualización).
- NOFRAME. Crea una ventana sin marco o barra de título si es posible.
- HWACCEL. Utiliza aceleración de hardware.
- SRCCOLORKEY. Uso de la superficie colorkey blitting.
- RLEACCEL. El blitting de Colorkey se acelera con RLE.
- SRCALPHA. Utiliza una mezcla alfa.
Podemos utilizar varios flags simultáneamente:
screen = SDL.Screen.set_video_mode (640, 480, 30, SurfaceFlag.FULLSCREEN | SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE)
✔ Ahora entramos en el “bucle principal” del programa. Aquí verificamos eventos y luego actualizamos la pantalla.
✔ Cuando se cierra la ventana o se presiona la tecla ESC, se termina el bucle principal.
✔ Salimos con SDL.quit ().
Podemos presentar el código de manera más limpia definiendo las variables al inicio, lo que además permite modificarlas más fácilmente:
// compila con valac --pkg sdl nombre_archivo.gs uses SDL init titulo:string = "Genie SDL Demo" ancho:int = 640 alto:int = 480 bpp:int = 30 flags:uint32 = SurfaceFlag.FULLSCREEN screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) while (gameover == false) SDL.Event.wait (out evento) if(evento.type == SDL.EventType.QUIT) gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break screen.flip () SDL.quit ()
Observarás pequeños cambios en el bucle principal, es otra manera de escribirlo, aunque no hace exactamente lo mismo: en este caso el programa está a la espera de un evento, y aunque para imágenes estáticas nos puede servir, en caso de imágenes en movimiento las detiene. Pasa lo mismo aquí:
while (gameover == false) SDL.Event.wait (out evento) case evento.type when SDL.EventType.QUIT gameover = true break when EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break screen.flip ()
¡A dibujar!
SDLGraphics ofrece varias herramientas para dibujar (ver en Valadoc):
- Pixel
- Line
- Rectangle
- Circle
- Ellipse
- Arc
- Trigon
- Polygon
- BezierCurve
- Text
- RotoZoom
- Filter
He probado algunas de ellas para recrear la pantalla gráfica del clásico juego de tenis de los primeros videojuegos. Éste es el resultado (además al pasar el ratón sobre la pantalla, los dibujos cambian de color):
El código empleado es éste:
// compila con valac --pkg sdl --pkg sdl-gfx -X -lSDL_gfx nombre_archivo.gs uses SDL uses SDLGraphics init titulo:string = "Genie SDL Demo" ancho:int = 640 alto:int = 480 bpp:int = 32 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE | SurfaceFlag.ANYFORMAT screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) while (gameover == false) SDL.Event.wait (out evento) if(evento.type == SDL.EventType.QUIT) gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break color:uint32 = Random.next_int () Rectangle.fill_color(screen, 60, 200, 80, 280, color) color2:uint32 = Random.next_int () Rectangle.fill_color(screen, 560, 200, 580, 280, color2) color3:uint32 = Random.next_int () Circle.fill_color (screen, 100, 240, 10, color3) color4:uint32 = Random.next_int () Line.color(screen, 320, 40, 320, 440, color4) Text.color(screen, 280, 10, "40", color) Text.color(screen, 340, 10, "15", color2) screen.flip() SDL.quit ()
Si no queremos que la pelota cambie de color, la pintamos de amarillo:
Circle.fill_rgba (screen, 100, 240, 10, 255, 255, 0, 128)
Presentar una imagen
SDLImage (y sdl2-image) facilita cargar y mostrar una imagen en la pantalla. Para ello podemos utilizar varios métodos:
- Cargar la imagen directamente con SDLImage.load, o
- Con SDL.RWops, una estructura de transferencia de datos de entrada y salida (I/O) para leer y escribir (RW: read-write) en la memoria (SDL.RWops.from_mem) y en un archivo (SDL.RWops.from_file), que es lo que nos interesa ahora.
Un ejemplo del primer método:
// valac --pkg sdl --pkg sdl-gfx --pkg sdl-image -X -lSDL_gfx -X -lSDL_image nombre_archivo.gs uses SDL uses SDLGraphics uses SDLImage init titulo:string = "Genie SDL Demo" ancho:int = 640 alto:int = 640 bpp:int = 32 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWSURFACE screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) imagen: SDL.Surface imagen = SDLImage.load("sdl.bmp") // Si no carga la imagen prueba ver el tipo de error con // stderr.printf("Error: %s\n", SDL.get_error() ) // quizá te falta alguna librería while (gameover == false) SDL.Event.wait (out evento) if(evento.type == SDL.EventType.QUIT) gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break imagen.blit (null, screen, null) screen.flip() SDL.quit ()
Utilizando el segundo método, una opción es:
imagen: SDL.Surface src: SDL.RWops src = new SDL.RWops.from_file("/home/jcv/Programacion/Genie/SDL/sdl.bmp", "rb") imagen = SDLImage.load_rw(src)
O también así, especificando el tipo de imagen (ejemplos: load_bmp, load_gif, load_png, load_jpg):
imagen: SDL.Surface src: SDL.RWops src = new SDL.RWops.from_file("/home/jcv/Programacion/Genie/SDL/sdl.bmp", "rb") imagen = SDLImage.load_bmp(src)
Ahora vemos nuestra imagen en la pantalla, pero nos falta situarla en el lugar que nos interesa.
Antes hemos utilizado blit para realizar una copia rápida en un destino (dst) que era screen. Otros parámetros son srcrect y dstrect. El primero representa el espacio (un rectángulo) de la imagen que se copia en destino (una parte de la imagen, o null para copiar toda la superficie). Y el segundo parámetro, dstrect, representa el espacio (otro rectángulo) del destino donde se copia.
Por tanto, con srcrect (src + rect) seleccionamos qué parte de la imagen fuente copiamos, y con dstrect (destino + rect) seleccionamos donde la copiamos.
Los campos de estos rectángulos (Rect) son la posición y el tamaño: x (int16), y (int16), w (uint16), h (uint16).
Por ejemplo, para centrar la imagen:
// valac --pkg sdl --pkg sdl-gfx --pkg sdl-image -X -lSDL_gfx -X -lSDL_image nombre_fichero.gs uses SDL uses SDLGraphics uses SDLImage init titulo:string = "Genie SDL Demo" ancho:int16 = 640 // utilizamos int16 alto:int16 = 640 // utilizamos int16 bpp:int = 32 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWSURFACE screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event dr: SDL.Rect x: int16 y: int16 SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) imagen: SDL.Surface imagen = SDLImage.load("sdl.bmp") while (gameover == false) SDL.Event.wait (out evento) if(evento.type == SDL.EventType.QUIT) gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break x = (ancho-408)/2 // 408 es el ancho de la imagen y = (alto-167)/2 // 167 es la altura de la imagen dr = {x, y, 408, 167} imagen.blit (null, screen, dr) screen.flip() SDL.quit ()
Con este resultado:
Aunque también podemos obtener directamente las dimensiones de la imagen, así:
// valac --pkg sdl --pkg sdl-gfx --pkg sdl-image -X -lSDL_gfx -X -lSDL_image nombre_archivo.gs uses SDL uses SDLGraphics uses SDLImage init titulo:string = "Genie SDL Demo" ancho:int16 = 640 // utilizamos int16 alto:int16 = 640 // utilizamos int16 bpp:int = 32 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWSURFACE screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event dr: SDL.Rect an: int al: int anch: int16 alt: int16 x: int16 y: int16 SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) imagen: SDL.Surface imagen = SDLImage.load("sdl.bmp") // obtenemos el ancho y alto de la imagen como int // y lo pasamos a int16 an = imagen.w al = imagen.h anch = (int16)an alt = (int16)al while (gameover == false) SDL.Event.wait (out evento) if(evento.type == SDL.EventType.QUIT) gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break x = (ancho-anch)/2 y = (alto-alt)/2 dr = {x, y, anch, alt} imagen.blit (null, screen, dr) screen.flip() SDL.quit ()
En marcha
Volvemos al ejemplo del juego retro de tenis para ¡crear movimiento!
// compila con valac --pkg sdl --pkg sdl-gfx -X -lSDL_gfx nombre_archivo.gs uses SDL uses SDLGraphics init titulo:string = "Genie SDL Demo" ancho:int16 = 640 alto:int16 = 480 bpp:int = 16 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE | SurfaceFlag.ANYFORMAT screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event var x = 100 var y = 240 var new_x = 2 var new_y = 2 SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) while (!gameover) while (Event.poll (out evento)) == 1 if evento.type ==SDL.EventType.QUIT gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break // rebota en las paredes if x > ancho or x < 0 new_x = -new_x if y > alto or y < 0 new_y = -new_y // rebota en los rectángulos de los jugadores if (x > 50 and x < 75) and (y >= 200 and y <= 280) if y == 200 or y == 280 new_y = -new_y else new_x = -new_x if (x > 565 and x < 590) and (y >= 200 and y <= 280) if y == 200 or y == 280 new_y = -new_y else new_x = -new_x x = x + new_x y = y + new_y Line.rgba(screen, 320, 40, 320, 440, 255, 255, 255, 128) Rectangle.fill_rgba(screen, 0, 0, ancho, alto, 0, 128, 255, 128) Rectangle.fill_rgba(screen, 60, 200, 65, 280, 255, 255, 255, 200) Rectangle.fill_rgba(screen, 575, 200, 580, 280, 255, 255, 255, 200) Circle.fill_rgba (screen, x, y, 10, 255, 255, 0, 200) Circle.outline_rgba_aa (screen, x, y, 10, 255, 255, 0, 200) screen.flip() SDL.quit ()
El resultado es más o menos así:
Con estos elementos ya podemos empezar a hacer el juego un poco interactivo. Para ello hacemos que la posición vertical (y) de los jugadores deje de ser fija y dependa de unas variables (pos_1 y pos_2 en el ejemplo) que cambian de valor al pulsar unas teclas (a y z para el jugador de la izquierda y arriba y abajo para el jugador de la derecha). Además tenemos que incorporar esas variables al efecto de rebote de la pelota. El código quedaría así:
// compila con valac --pkg sdl --pkg sdl-gfx -X -lSDL_gfx nombre_archivo.gs uses SDL uses SDLGraphics init titulo:string = "Genie SDL Demo" ancho:int16 = 640 alto:int16 = 480 bpp:int = 16 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE | SurfaceFlag.ANYFORMAT screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event var x = 100 var y = 240 var new_x = 2 var new_y = 2 var pos_1 = 200 var pos_2 = 200 SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) while (!gameover) while (Event.poll (out evento)) == 1 if evento.type ==SDL.EventType.QUIT gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break // jugador 1 if evento.key.keysym.sym == KeySymbol.a if pos_1 != 0 // si no llega al tope superior de la ventana pos_1 = pos_1 - 5 if evento.key.keysym.sym == KeySymbol.z if pos_1 != 400 // si no llega al tope inferior de la ventana pos_1 = pos_1 + 5 // jugador 2 if evento.key.keysym.sym == KeySymbol.UP if pos_2 != 0 // si no llega al tope superior de la ventana pos_2 = pos_2 - 5 if evento.key.keysym.sym == KeySymbol.DOWN if pos_2 != 400 // si no llega al tope inferior de la ventana pos_2 = pos_2 + 5 // rebota en las paredes if x > ancho or x < 0 new_x = -new_x if y > alto or y < 0 new_y = -new_y // rebota en los rectángulos de los jugadores // jugador 1 if (x > 60 and x < 65) and (y >= pos_1 and y <= pos_1+80) if y == pos_1 or y == pos_1+80 new_y = -new_y else new_x = -new_x // jugador 2 if (x > 575 and x < 580) and (y >= pos_2 and y <= pos_2+80) if y == pos_2 or y == pos_2+80 new_y = -new_y else new_x = -new_x x = x + new_x y = y + new_y // fondo y linea central Line.rgba(screen, 320, 40, 320, 440, 255, 255, 255, 128) Rectangle.fill_rgba(screen, 0, 0, ancho, alto, 0, 128, 255, 128) // jugadores Rectangle.fill_rgba(screen, 60, pos_1, 65, pos_1+80, 255, 255, 255, 200) Rectangle.fill_rgba(screen, 575, pos_2, 580, pos_2+80, 255, 255, 255, 200) // pelota Circle.fill_rgba (screen, x, y, 10, 255, 255, 0, 200) Circle.outline_rgba_aa (screen, x, y, 10, 255, 255, 0, 200) screen.flip() SDL.quit ()
Sonido
Al código anterior le podemos añadir algún efecto de sonido utilizando SDLMixer, en este caso el sonido se escucha cada vez que un jugador golpea la pelota:
// compila con valac --pkg sdl --pkg sdl-gfx -X -lSDL_gfx --pkg sdl-mixer -X -lSDL_mixer nombre_archivo.gs uses SDL uses SDLGraphics uses SDLMixer // sonido archivo: SDL.RWops sonido: SDLMixer.Chunk canal: SDLMixer.Channel init titulo:string = "Genie SDL Demo" ancho:int16 = 640 alto:int16 = 480 bpp:int = 16 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE | SurfaceFlag.ANYFORMAT screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event var x = 100 var y = 240 var new_x = 2 var new_y = 2 var pos_1 = 200 var pos_2 = 200 SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) // sonido SDLMixer.open(44100,SDL.AudioFormat.S16LSB,2,4096) archivo= new SDL.RWops.from_file ("/home/jcv/Programacion/Genie/SDL/Boing.ogg","rb") sonido= new SDLMixer.Chunk.WAV (archivo,-1) canal.play(sonido,0) // sonido de saque while (!gameover) while (Event.poll (out evento)) == 1 if evento.type ==SDL.EventType.QUIT gameover = true break else if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE gameover = true break // jugador 1 if evento.key.keysym.sym == KeySymbol.a if pos_1 != 0 // si no llega al tope superior de la ventana pos_1 = pos_1 - 5 if evento.key.keysym.sym == KeySymbol.z if pos_1 != 400 // si no llega al tope inferior de la ventana pos_1 = pos_1 + 5 // jugador 2 if evento.key.keysym.sym == KeySymbol.UP if pos_2 != 0 // si no llega al tope superior de la ventana pos_2 = pos_2 - 5 if evento.key.keysym.sym == KeySymbol.DOWN if pos_2 != 400 // si no llega al tope inferior de la ventana pos_2 = pos_2 + 5 // rebota en las paredes if x > ancho or x < 0 new_x = -new_x if y > alto or y < 0 new_y = -new_y // rebota en los rectángulos de los jugadores // jugador 1 if (x > 60 and x < 65) and (y >= pos_1 and y <= pos_1+80) if y == pos_1 or y == pos_1+80 new_y = -new_y else new_x = -new_x canal.play(sonido,0) // jugador 2 if (x > 575 and x < 580) and (y >= pos_2 and y <= pos_2+80) if y == pos_2 or y == pos_2+80 new_y = -new_y else new_x = -new_x canal.play(sonido,0) x = x + new_x y = y + new_y // fondo y linea central Line.rgba(screen, 320, 40, 320, 440, 255, 255, 255, 128) Rectangle.fill_rgba(screen, 0, 0, ancho, alto, 0, 128, 255, 128) // jugadores Rectangle.fill_rgba(screen, 60, pos_1, 65, pos_1+80, 255, 255, 255, 200) Rectangle.fill_rgba(screen, 575, pos_2, 580, pos_2+80, 255, 255, 255, 200) // pelota Circle.fill_rgba (screen, x, y, 10, 255, 255, 0, 200) Circle.outline_rgba_aa (screen, x, y, 10, 255, 255, 0, 200) screen.flip() SDL.quit ()
¡A jugar!
En el ejemplo que estamos utilizando para crear un juego de tenis, hasta ahora hemos desarrollado un código que funciona (que no es poco), pero seamos sinceros, la jugabilidad brilla por su ausencia. Para que realmente se pueda llamar juego (que por lo menos sea divertido para nosotros, no hace falta que sea comercial) debemos pulir el código, y principalmente eso pasa por tres objetivos:
- Habilitar el desplazamiento continuo de los jugadores (moverse mientras una tecla está pulsada),
- incorporar un marcador (se trata de saber quien gana y quien pierde), y
- añadir mejoras, corregir errores y optimizar el código.
Manos a la obra. Para conseguir el primer objetivo (y de paso vamos optimizando el código) necesitamos distinguir entre los eventos de presionar y soltar las teclas que utilizan los jugadores. El código nos quedaría así:
// compila con valac --pkg sdl --pkg sdl-gfx -X -lSDL_gfx --pkg sdl-mixer -X -lSDL_mixer nombre_archivo.gs uses SDL uses SDLGraphics uses SDLMixer // sonido archivo: SDL.RWops sonido: SDLMixer.Chunk canal: SDLMixer.Channel init titulo:string = "Genie SDL Demo" ancho:int16 = 640 alto:int16 = 480 bpp:int = 16 flags:uint32 = SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE | SurfaceFlag.ANYFORMAT screen: unowned SDL.Screen gameover:bool = false evento: SDL.Event var x = 100 var y = 240 var new_x = 2 var new_y = 2 var pos1 = 200 var pos2 = 200 var pos1_new = 0 var pos2_new = 0 SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) // sonido SDLMixer.open(44100,SDL.AudioFormat.S16LSB,2,4096) archivo= new SDL.RWops.from_file ("/home/jcv/Programacion/Genie/SDL/Boing.ogg","rb") sonido= new SDLMixer.Chunk.WAV (archivo,-1) canal.play(sonido,0) // sonido de saque while (!gameover) while (Event.poll (out evento)) == 1 case evento.type when SDL.EventType.QUIT gameover = true break when EventType.KEYDOWN case evento.key.keysym.sym when KeySymbol.ESCAPE gameover = true break when KeySymbol.a pos1_new = -1 break when KeySymbol.z pos1_new = +1 break when KeySymbol.UP pos2_new = -1 break when KeySymbol.DOWN pos2_new = +1 break when EventType.KEYUP case evento.key.keysym.sym when KeySymbol.a pos1_new = 0 break case evento.key.keysym.sym when KeySymbol.z pos1_new = 0 break case evento.key.keysym.sym when KeySymbol.UP pos2_new = 0 break case evento.key.keysym.sym when KeySymbol.DOWN pos2_new = 0 break // actualiza la posición de los jugadores pos1 += pos1_new pos2 += pos2_new // evita que los jugadores se salgan de la pantalla if pos1 < 0 do pos1 = -1 if pos1 > 400 do pos1 = 401 if pos2 < 0 do pos2 = -1 if pos2 > 400 do pos2 = 401 // rebota en las paredes if x > ancho or x < 0 new_x = -new_x if y > alto or y < 0 new_y = -new_y // rebota en los rectángulos de los jugadores // jugador 1 if (x > 60 and x < 65) and (y >= pos1 and y <= pos1+80) if y == pos1 or y == pos1+80 new_y = -new_y else new_x = -new_x canal.play(sonido,0) // jugador 2 if (x > 575 and x < 580) and (y >= pos2 and y <= pos2+80) if y == pos2 or y == pos2+80 new_y = -new_y else new_x = -new_x canal.play(sonido,0) x = x + new_x y = y + new_y // fondo y linea central Line.rgba(screen, 320, 40, 320, 440, 255, 255, 255, 128) Rectangle.fill_rgba(screen, 0, 0, ancho, alto, 0, 128, 255, 128) // jugadores Rectangle.fill_rgba(screen, 60, pos1, 65, pos1+80, 255, 255, 255, 200) Rectangle.fill_rgba(screen, 575, pos2, 580, pos2+80, 255, 255, 255, 200) // pelota Circle.fill_rgba (screen, x, y, 10, 255, 255, 0, 200) Circle.outline_rgba_aa (screen, x, y, 10, 255, 255, 0, 200) screen.flip() SDL.quit ()
¡Ahora sí! En poco más de 100 líneas de código ya tenemos algo parecido a un juego. Pero vamos a seguir mejorándolo.
El siguiente paso será añadir un marcador. Necesitamos unas variables con la que operar sumas, por ejemplo, enteros (int), aunque para mostrarlas en la pantalla más adelante las convertiremos en cadenas de texto:
mar1:int = 0 mar2:int = 0 marca1:string marca2: string
En nuestro juego se consigue un punto cuando la pelota toca la pared trasera del oponente y, a diferencia del clásico juego de tenis, el juego continúa (por lo que los rebotes pueden hacer ganar más de un punto, incluso si rebota en la 'espalda' del jugador y vuelve a tocar la pared).
// rebota en las paredes if x > ancho or x < 0 if x < 0 mar2 += 1 if x > ancho mar1 += 1 new_x = -new_x if y > alto or y < 0 new_y = -new_y
Mostramos el marcador en pantalla:
// marcador marca1 = mar1.to_string() marca2 = mar2.to_string() Text.rgba(screen, 280, 10, marca1, 255, 255, 255, 200) Text.rgba(screen, 340, 10, marca2, 255, 255, 255, 200)
Y finalmente el juego termina cuando alguno de los jugadores gana al alcanzar 21 puntos (previamente definimos fin:bool = false):
if marca1 == "21" or marca2 == "21" fin = true while (fin == true) Text.rgba(screen, 260, 220, "JUEGO TERMINADO", 255, 255, 255, 200) Text.rgba(screen, 260, 260, "Inicio: ESC / Otro Juego: J", 255, 255, 255, 200) screen.flip() SDL.Event.wait (out evento) if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE // reiniciar variables x = 100 y = 240 new_x = 0 new_y = 0 pos1 = 200 pos2 = 200 pos1_new = 0 pos2_new = 0 mar1 = 0 mar2 = 0 fin = false gameover = true saque = true intro = false break if evento.key.keysym.sym == KeySymbol.j // reiniciar variables x = 100 y = 240 new_x = 0 new_y = 0 pos1 = 200 pos2 = 200 pos1_new = 0 pos2_new = 0 mar1 = 0 mar2 = 0 fin = false saque = true break
Otra mejora interesante es que el juego se inicie en una pantalla de bienvenida que muestra los controles que sirven para mover las raquetas y para interactuar con el programa (iniciar, salir, pausar). Esto lo he resuelto creando un nuevo bucle que a su vez contiene dos bucles que se corresponden con una pantalla de inicio (que se ejecuta en el arranque) y con una pantalla del juego propiamente dicho que se activa con un evento desde el primer bucle. También se ha programado la posibilidad de volver a la primera pantalla desde la segunda.
Finalmente, otras pequeñas mejoras han sido:
- La pelota no se pone en movimiento hasta que el jugador 'saca'.
- La dirección de la pelota en el saque tiene cierta aleatoriedad.
- Posibilidad de pausar y continuar el juego.
- Nuevo sonido cuando se falla.
- Juego a pantalla completa.
- No aparece el cursor en la pantalla durante el juego.
Posiblemente, algunas de mis opciones no son las soluciones óptimas (te invito a mejorar el código y compartirlo en esta wiki), pero ten en cuenta que ha sido mi primera experiencia con Genie + SDL, y aunque seguramente es mejorable, creo que el resultado es aceptable. Además, el programa se podría pulir mucho más (por ejemplo añadiendo gráficos, efectos en el texto, velocidad variable de la pelota y muchas cosas más), pero creo que como primera aproximación es suficiente. Disfruta del juego:
- Descarga: tenis.gs
// compila con valac --pkg sdl --pkg sdl-gfx -X -lSDL_gfx --pkg sdl-mixer -X -lSDL_mixer nombre_archivo.gs uses SDL uses SDLGraphics uses SDLMixer // sonido archivo1: SDL.RWops archivo2: SDL.RWops sonido1: SDLMixer.Chunk sonido2: SDLMixer.Chunk canal1: SDLMixer.Channel canal2: SDLMixer.Channel init titulo:string = "Genie SDL Demo" ancho:int16 = 640 alto:int16 = 480 bpp:int = 16 flags:uint32 = SurfaceFlag.FULLSCREEN | SurfaceFlag.DOUBLEBUF | SurfaceFlag.HWACCEL | SurfaceFlag.HWSURFACE | SurfaceFlag.ANYFORMAT screen: unowned SDL.Screen tenis:bool = false juego: SDL.Event intro:bool = false inicio: SDL.Event gameover:bool = true evento: SDL.Event pausa:bool = false saque: bool = true fin:bool = false var x = 100 var y = 240 var new_x = 0 var new_y = 0 var pos1 = 200 var pos2 = 200 var pos1_new = 0 var pos2_new = 0 mar1:int = 0 mar2:int = 0 marca1:string marca2: string SDL.init(SDL.InitFlag.VIDEO) SDL.WindowManager.set_caption (titulo, "") screen = SDL.Screen.set_video_mode (ancho, alto, bpp, flags) //oculta cursor SDL.Cursor.show(0) // sonidos SDLMixer.open(44100,SDL.AudioFormat.S16LSB,2,4096) archivo1 = new SDL.RWops.from_file ("/home/jcv/Programacion/Genie/SDL/Boing.ogg","rb") sonido1 = new SDLMixer.Chunk.WAV (archivo1,-1) archivo2 = new SDL.RWops.from_file ("/home/jcv/Programacion/Genie/SDL/error.ogg","rb") sonido2 = new SDLMixer.Chunk.WAV (archivo2,-1) while (!tenis) while (Event.poll (out juego)) == 1 // pantalla de inicio while (!intro) while (Event.poll (out inicio)) == 1 case inicio.type when SDL.EventType.QUIT intro = true tenis = true break when EventType.KEYDOWN case inicio.key.keysym.sym when KeySymbol.ESCAPE intro = true tenis = true break when KeySymbol.i intro = true gameover = false break Rectangle.fill_rgba(screen, 0, 0, ancho, alto, 0, 0, 0, 200) Text.rgba(screen, 260, 100, "JUEGO DE TENIS RETRO", 255, 255, 255, 200) Text.rgba(screen, 300, 180, "CONTROLES", 255, 255, 255, 200) Text.rgba(screen, 160, 200, "Inicio: I Salir : ESC Pausa: P", 255, 255, 255, 200) Text.rgba(screen, 160, 240, " Jugador 1 Jugador 2", 255, 255, 255, 200) Text.rgba(screen, 160, 260, "Subir: A Up", 255, 255, 255, 200) Text.rgba(screen, 160, 280, "Bajar: Z Down", 255, 255, 255, 200) Text.rgba(screen, 160, 300, "Sacar: Q", 255, 255, 255, 200) screen.flip() // pantalla de juego while (!gameover) while (Event.poll (out evento)) == 1 case evento.type when SDL.EventType.QUIT gameover = true intro = false break when EventType.KEYDOWN case evento.key.keysym.sym when KeySymbol.ESCAPE // reiniciar variables x = 100 y = 240 new_x = 0 new_y = 0 pos1 = 200 pos2 = 200 pos1_new = 0 pos2_new = 0 mar1 = 0 mar2 = 0 fin = false gameover = true saque = true intro = false break when KeySymbol.q if saque == true new_x = 2 var saque_y = Random.int_range (1, 3) if saque_y == 1 new_y = -2 else new_y = 2 canal1.play(sonido1,0) // sonido de saque saque = false when KeySymbol.p //pausa if pausa == false pausa = true else if pausa == true pausa = false break when KeySymbol.a pos1_new = -1 break when KeySymbol.z pos1_new = +1 break when KeySymbol.UP pos2_new = -1 break when KeySymbol.DOWN pos2_new = +1 break when EventType.KEYUP case evento.key.keysym.sym when KeySymbol.a pos1_new = 0 break case evento.key.keysym.sym when KeySymbol.z pos1_new = 0 break case evento.key.keysym.sym when KeySymbol.UP pos2_new = 0 break case evento.key.keysym.sym when KeySymbol.DOWN pos2_new = 0 break // pausa while (pausa == true) SDL.Event.wait (out evento) Text.rgba(screen, 260, 240, "JUEGO EN PAUSA (P)", 255, 255, 255, 200) screen.flip() if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.p pausa = false break // actualiza la posición de los jugadores pos1 += pos1_new pos2 += pos2_new // evita que los jugadores se salgan de la pantalla if pos1 < 0 do pos1 = -1 if pos1 > 400 do pos1 = 401 if pos2 < 0 do pos2 = -1 if pos2 > 400 do pos2 = 401 // rebota en las paredes if x > ancho or x < 0 canal2.play(sonido2,0) if x < 0 mar2 += 1 if x > ancho mar1 += 1 new_x = -new_x if y > alto or y < 0 new_y = -new_y // rebota en los rectángulos de los jugadores // jugador 1 if (x > 60 and x < 65) and (y >= pos1 and y <= pos1+80) if y == pos1 or y == pos1+80 new_y = -new_y else new_x = -new_x canal1.play(sonido1,0) // jugador 2 if (x > 575 and x < 580) and (y >= pos2 and y <= pos2+80) if y == pos2 or y == pos2+80 new_y = -new_y else new_x = -new_x canal1.play(sonido1,0) x = x + new_x y = y + new_y // fondo y linea central Line.rgba(screen, 320, 40, 320, 440, 255, 255, 255, 128) Rectangle.fill_rgba(screen, 0, 0, ancho, alto, 0, 128, 255, 128) // jugadores Rectangle.fill_rgba(screen, 60, pos1, 65, pos1+80, 255, 255, 255, 200) Rectangle.fill_rgba(screen, 575, pos2, 580, pos2+80, 255, 255, 255, 200) // pelota Circle.fill_rgba (screen, x, y, 10, 255, 255, 0, 200) Circle.outline_rgba_aa (screen, x, y, 10, 255, 255, 0, 200) // marcador marca1 = mar1.to_string() marca2 = mar2.to_string() Text.rgba(screen, 280, 10, marca1, 255, 255, 255, 200) Text.rgba(screen, 340, 10, marca2, 255, 255, 255, 200) if marca1 == "21" or marca2 == "21" fin = true while (fin == true) Text.rgba(screen, 260, 220, "JUEGO TERMINADO", 255, 255, 255, 200) Text.rgba(screen, 260, 260, "Inicio: ESC / Otro Juego: J", 255, 255, 255, 200) screen.flip() SDL.Event.wait (out evento) if evento.type == EventType.KEYDOWN if evento.key.keysym.sym == KeySymbol.ESCAPE // reiniciar variables x = 100 y = 240 new_x = 0 new_y = 0 pos1 = 200 pos2 = 200 pos1_new = 0 pos2_new = 0 mar1 = 0 mar2 = 0 fin = false gameover = true saque = true intro = false break if evento.key.keysym.sym == KeySymbol.j // reiniciar variables x = 100 y = 240 new_x = 0 new_y = 0 pos1 = 200 pos2 = 200 pos1_new = 0 pos2_new = 0 mar1 = 0 mar2 = 0 fin = false saque = true break screen.flip() screen.flip() SDL.quit ()
Dejo a continuación una carpeta comprimida (zip) que contiene el archivo compilado (en xubuntu 16) y los dos archivos de sonido necesarios:
Descarga: tenis.zip
También dejo para descargar en paquete deb, para instalar directamente (por ejemplo, con el Instalador de paquetes GDebi que indicará si se satisfacen las dependencias, que son libsdl1.2debian, libsdl-gfx1.2-5 y libsdl-mixer1.2):
Descarga: tenisretro_0.1_all.deb
El programa se instala en /usr/bin/TENIS y desde allí se ejecuta con ./tenis