Interfaz SDL

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:

  1. Cargar la imagen directamente con SDLImage.load, o
  2. 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í:

TENIS SDL

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:

  1. Habilitar el desplazamiento continuo de los jugadores (moverse mientras una tecla está pulsada),
  2. incorporar un marcador (se trata de saber quien gana y quien pierde), y
  3. 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

🔝