Aplicaciones portables entre PSP y GNU/Linux con SDL
Aquí estamos de nuevo para dar un poco de caña a nuestras PSP's, ya sabemos cómo desarrollar aplicaciones multimedia para la consola, ahora vamos a dar dos pasos más: las aplicaciones usarán SDL y serán portables entre nuestro GNU/Linux y nuestra PSP.
Introducción
A todos nos gusta programar de vez en cuando alguna aplicación que use gráficos. Para los que tengáis experiencia probablemente conoceréis libSDL, se trata de un conjunto de bibliotecas que nos facilitan el acceso dispositivos de entrada como teclados y joysticks; y dispositivos de salida como tarjetas de vídeo y de sonido. Además incluyen utilidades como cargadores de archivos de imagen o de sonido. Son muy fáciles de usar y están portadas a muchas arquitecturas, entre ellas la PSP. Por otro lado, son muchas las ventajas de programar una aplicación portable. En nuestro caso la ventaja es evidente: podemos probar nuestro programa sin necesidad de pasarlo a la consola. Para poder seguir correctamente esta receta es indispensable la receta de "Programación de la PSP: Intros multimedia" y "Kit de desarrollo completo para PSP".Nuestra aplicación
Bueno, a parte de libSDL, existen más librerías especializadas para ayudar a la carga y manipulación de imágenes, para el uso del sonido y la mezcladora, incluso para el uso de fuentes true type. Vamos a hacer una aplicación que use todo esto a la vez :P. Por un lado cargaremos un gráfico de fondo, sobre él pondremos otro gráfico al que aplicaremos efectos de escalado. También mostraremos texto que crearemos nosotros y todo esto amenizado con alguna cancioncilla.La biblioteca SDL
Bien, el sistema SDL se divide en varios subsistemas, uno de ellos, por ejemplo, es SDL_Video. Antes de empezar a usar SDL debemos iniciar estos sistemas (y finalizarlos después antes de terminar la aplicación), la estructura básica de una aplicación de este tipo será:#include <SDL/SDL.h> int main(int argc, char *argv[]) { /* Inicializamos nuestras variables y tal */ ... /* Inicializamos dos subsistemas */ SDL_Init(SDL_INIT_VIDEO|SDL_INIT_SOUND); /* Bucle principal */ do { /* Hacemos nuestras cosas */ ... } while (!done); /* Finalizamos SDL */ SDL_Quit(); exit(0); }Para manejar imágenes emplearemos las SDL_Surfaces, éstas nos permiten tratar tanto las imágenes que usemos como las ventanas de visualización de una forma homogénea. En un principio SDL sólo permite crear surfaces a partir de imágenes BMP, pero gracias a la biblioteca SDL_image podemos cargar también archivos JPG y PNG. Como sabéis, los archivos PNG codifican los píxeles en RGBA (canales de color y de transparencia), en SDL se nos creará una capa con información de transparencia, algo muy útil. En cuando a la creación de la ventana de visualización, será una SDL_Surface especial, donde lo que dibujemos se mostrará por pantalla:
#include <SDL/SDL.h> #include <SDL/SDL_image.h> /* Inicializacion de variables, si, son globales :( */ SDL_Surface *screen = NULL; SDL_Surface *src_buffer = NULL; SDL_Surface *background = NULL; SDL_Rect dest; ... int main(int argc, char *argv[]) { SDL_Init(SDL_INIT_VIDEO|SDL_INIT_SOUND); /* Creamos la surface de la pantalla */ screen = SDL_SetVideoMode(480, 272, 16, SDL_HWSURFACE); /* Y el buffer donde dibujaremos antes de volcar a pantalla */ scr_buffer = IMG_Load("background.png"); /* Cargamos el fondo */ background = IMG_Load("background.png"); do { /* Dibujamos el fondo, primero al buffer */ dest.x = 0; dest.y = 0; SDL_BlitSurface(background, NULL, scr_buffer, &dest); /* Seguimos dibujando tontunas en el buffer */ ... /* Volcamos el buffer a la pantalla y ya tenemos el frame */ dest.x = 0; dest.y = 0; SDL_BlitSurface(scr_buffer, NULL, screen, &dest); SDL_Flip(screen); } while (!done); /* Finalizamos SDL */ SDL_Quit(); exit(0); }En cuanto al sonido, emplearemos las funciones de la biblioteca SDL_mixer. Con dichas funciones podremos cargar un archivo con alguna canción y reproducirla de forma automática en segundo plano:
#include <SDL/SDL.h> #include <SDL/SDL_image.h> #include <SDL/SDL_mixer.h> /* Inicializacion de variables, si, son globales :( */ ... Mix_Music *bgm = NULL; int music_channel; int main(int argc, char *argv[]) { SDL_Init(SDL_INIT_VIDEO|SDL_INIT_SOUND); ... /* Cargamos el fondo */ background = IMG_Load("background.png"); /* Cargamos la musica */ bgm = Mix_LoadMUS("am-fm.mod"); /* Antes del bucle principal comenzamos a reproducir */ music_channel = Mix_PlayMusic(bgm, -1); do { ... } while (!done); /* Finalizamos SDL */ SDL_Quit(); exit(0); }Ahora vamos a hacer un pequeño efectillo como demostración de la manipulación de surfaces: un zoom periódico a una imagen cualquiera. Para ello usaremos funciones de la biblioteca SDL_gfx; con esta biblioteca podremos hacer escalados y rotaciones a una capa, obteniendo otra capa con la imagen transformada. Las funciones están bastante optimizadas, aunque el escalado de ampliación consume muchos más recursos que el escalado de reducción. Vayamos con el ejemplo:
#include <SDL/SDL.h> #include <SDL/SDL_image.h> #include <SDL/SDL_mixer.h> #include <SDL/SDL_rotozoom.h> /* SDL_gfx */ /* Inicializacion de variables, si, son globales :( */ ... SDL_Surface *title = NULL; SDL_Surface *zoomed_title = NULL; SDL_Rect ztitle_rect; SDL_Rect title_dest; double zoom_factor; int frame; int main(int argc, char *argv[]) { SDL_Init(SDL_INIT_VIDEO|SDL_INIT_SOUND); ... /* Cargamos la capa */ title = IMG_Load("crysol_title.png"); do { /* Calculamos el factor de zoom */ zoom_factor = 0.5 + (cos(frame * 0.05) * 0.5); zoomed_title = zoomSurface(title, zoom_factor, zoom_factor, SMOOTHING_OFF); ... /* Calculamos la posicion de la esquina superior derecha para que */ /* quede el titulo centrado */ SDL_GetClipRect(zoomed_title, &ztitle_rect); xtitle = 240 - (ztitle_rect.w / 2); ytitle = 50 - (ztitle_rect.h / 2); title_dest.x = xtitle; title_dest.y = ytitle; /* Dibujamos el titulo en el buffer (que se volcaba a cada frame) */ SDL_BlitSurface(zoomed_title, NULL, scr_buffer, &title_dest); ... ++frame; } while (!done); /* Finalizamos SDL */ SDL_Quit(); exit(0); }Por último sólo nos queda poner los créditos. Para ello usamos una fuente TTF y la biblioteca SDL_ttf. Con sus funciones podemos cargar una fuente y crear surfaces con el texto renderizado. Para este ejemplo también aplicaremos escalado a estas surfaces de texto:
#include <SDL/SDL.h> #include <SDL/SDL_image.h> #include <SDL/SDL_mixer.h> #include <SDL/SDL_rotozoom.h> #include <SDL/SDL_ttf.h> /* Inicializacion de variables, si, son globales :( */ ... TTF_Font *font = NULL; SDL_Surface *text = NULL; SDL_Surface *ttext = NULL; SDL_Color c; int main(int argc, char *argv[]) { SDL_Init(SDL_INIT_VIDEO|SDL_INIT_SOUND); ... /* Cargamos la fuente */ font = TTF_OpenFont("arial.ttf", 12); TTF_SetFontStyle(font, TTF_STYLE_NORMAL); /* Color del texto */ c.r = 255; c.g = 255; c.b = 255; do { ... /* Renderizamos la fuente, la segunda en realidad no */ /* es necesaria, la hacemos para inicializar la surface */ text = TTF_RenderText_Blended(font, credito[ic], c); ttext = TTF_RenderText_Blended(font, credito[ic], c); ... /* Calculamos el factor de zoom para los creditos */ zoom_credit = zoom_credit + cz_factor; /* cz_factor es una variable que controlara el zoom */ /* de los creditos y cuándo cambiar de credito (ic)*/ ... /* Ahora escalamos el texto, calculamos su centro y al buffer */ ttext = zoomSurface(text, zoom_credit, zoom_credit, SMOOTHING_OFF); SDL_GetClipRect(ttext, &dest); dest.x = 240 - (dest.w / 2); dest.y = 200 - (dest.h / 2); SDL_BlitSurface(ttext, NULL, scr_buffer, &dest); ... } while (!done); /* Finalizamos SDL */ SDL_Quit(); exit(0); }Bien, estos simples efectos serán los elementos de nuestra mini-demo. Ahora vamos a ver cómo lograr la portabilidad entre GNU/Linux y nuestras PSP's.
Compilación
A la hora de crear el ejecutable para la PSP o el PC la principal diferencia estará en la compilación: las herramientas y librerías que se usan en un caso o en el otro. Para diferenciar ambos procesos he decidido crear dos makefiles: uno (por defecto) que creará el ejecutable normal y otro que creará el EBOOT.PBP para la PSP. Vamos primero con el primero, Makefile:SDL_CONFIG = sdl-config DEFAULT_CFLAGS = $(shell $(SDL_CONFIG) --cflags) MORE_CFLAGS = -O2 CFLAGS = $(DEFAULT_CFLAGS) $(MORE_CFLAGS) CXXFLAGS = $(DEFAULT_CFLAGS) $(MORE_CFLAGS) -fno-exceptions \ -fno-rtti LDLIBS = -lSDL_gfx -lSDL_image -lSDL_mixer -lSDL_ttf -lvorbisidec \ -lfreetype -lpng -ljpeg -lz -lm $(shell $(SDL_CONFIG) --libs) all: main main: main.c clean: rm -f *~ rm -f main.o mainComo véis es muy simple (y un poco chapucero), pero va bien. Ahora vamos con la versión para PSP, archivo Makefile.psp:
TARGET = Sdl_Example PSPSDK = $(shell psp-config --pspsdk-path) PSPBIN = $(shell psp-config --psp-prefix)/bin SDL_CONFIG = $(PSPBIN)/sdl-config OBJS = main.o DEFAULT_CFLAGS = $(shell $(SDL_CONFIG) --cflags) MORE_CFLAGS = -G0 -O2 -DPSP CFLAGS = $(DEFAULT_CFLAGS) $(MORE_CFLAGS) CXXFLAGS = $(DEFAULT_CFLAGS) $(MORE_CFLAGS) -fno-exceptions \ -fno-rtti LIBS = -lSDL_gfx -lSDL_image -lSDL_mixer -lSDL_ttf -lvorbisidec \ -lfreetype -lpng -ljpeg -lz -lm $(shell $(SDL_CONFIG) --libs) EXTRA_TARGETS = EBOOT.PBP include $(PSPSDK)/lib/build.makEste makefile no añade iconos ni gráficos al EBOOT.PBP por lo que saldrá uno muy feo... podéis cambiarlo como está en la otra receta recomendada. Para ejecutar el makefile por defecto:
Compilación condicional
Bien, ahora nos encontramos con un problema: queremos hacer un único programa que podamos compilar para ambas arquitecturas, sin embargo existen algunas diferencias a la hora de manejar ciertos aspectos que nos obligan a escribir código diferente para una u otra arquitectura. ¿Cómo se soluciona esto sin hacer varios programas distintos para las distintas arquitecturas?, pues con la compilación condicional. En la compilación condicional podemos tener un área de código que se compile o no según el resultado de una expresión. De esto no debemos abusar porque puede resultar un código poco legible y enrevesado. Debemos intentar escribir un código portable, es decir, que funcione en ambas arquitecturas (usando al máximo las herramientas que SDL nos ofrece) y sólo usar compilación condicional para aquellas partes que no podamos compartir. Gracias a SDL podemos inicializar todo el sistema de forma independiente al mismo, el único problema que vamos a tener es la terminación de la demo: en PC podemos emplear SDL pero en la PSP debemos instalar los ExitCallbacks que vimos en la otra receta. Estos Callbacks sólo debemos compilarlos cuando queramos generar el EBOOT.PBP. En el makefile que hicimos para PSP vimos que compilábamos con el siguiente parámetro: -DPSP, esto define el símbolo PSP y es ésto lo que podemos usar como expresión en la compilación condicional:#ifdef PSP /* Esto se compila cuando generamos codigo para PSP */ ... #endif #ifndef PSP /* Esto se compila si NO generamos codigo para PSP */ ... #endifEn el PC, para salir debemos leer de la cola de eventos de SDL y si existe un evento de tipo SDL_QUIT finalizamos. Como esto NO lo tenemos que usar cuando compilamos para PSP lo pondremos en bloques #ifndef PSP:
/* Inicializacion de variables, si, son globales :( */ ... #ifndef PSP SDL_Event event; #endif ... int main(int argc, char *argv[]){ ... /* Bucle principal */ do { ... #ifndef PSP while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { done = 1; break; } } #endif } while(!done); SDL_Quit(); exit(0); }Y ahora la parte de finalización para la PSP:
#ifdef PSP #include <pspkernel.h> #endif ... int main(int argc, char *argv[]){ #ifdef PSP SetupCallbacks(); #endif ... do { ... } while (!done); SDL_Quit(); #ifdef PSP sceKernelExitGame(); #endif exit(0); } #ifdef PSP /* Manejador del callback de salida */ int exit_callback(int arg1, int arg2, void *common) { done = 1; return 0; } /* Crea el callback de salida */ int CallbackThread(SceSize args, void *argp) { int cbid; cbid = sceKernelCreateCallback("Exit Callback", exit_callback, NULL); sceKernelRegisterExitCallback(cbid); sceKernelSleepThreadCB(); return 0; } /* Crea y configura el hilo para el callback y retorna su ID */ int SetupCallbacks(void) { int thid = 0; thid = sceKernelCreateThread("update_thread", CallbackThread, 0x11, 0xFA0, 0, 0); if(thid >= 0) { sceKernelStartThread(thid, 0, 0); } return thid; } #endifY con esto ya podremos finalizar la aplicación en la PSP de forma normal, de no ser así, la demo nunca finalizaría (bueno... apagando a lo bestia si...).
Apéndice A: El programa completo
Este código es bastante feo, tiene variables globales y está muy poco optimizado; pero eso es otra historia ;-):/* SDL Programming example for PSP by Int-0 (CRySoL) 22 jul 2007 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <SDL/SDL.h> #include <SDL/SDL_image.h> #include <SDL/SDL_mixer.h> #include <SDL/SDL_rotozoom.h> #include <SDL/SDL_ttf.h> #ifdef PSP #include <pspkernel.h> #endif #define CREDITS 20 /* Algunos valores */ int done; int use_sound; int frame; #ifndef PSP SDL_Event event; #endif int xtitle; int ytitle; double zoom_factor; double zoom_credit; double cz_factor; int c_wait; /* SDL sourfaces */ SDL_Surface * screen = NULL; SDL_Surface * scr_buffer = NULL; SDL_Surface * background = NULL; SDL_Surface * title = NULL; SDL_Surface * zoomed_title = NULL; /* Musica */ Mix_Music * bgm = NULL; int music_channel; /* Areas */ SDL_Rect dest; SDL_Rect ztitle_rect; SDL_Rect title_dest; /* Fuentes */ TTF_Font * font = NULL; SDL_Surface * text = NULL; SDL_Surface * ttext = NULL; /* Timer */ Uint32 lastTock = 0; unsigned const int speed = 150; char *credits[CREDITS]; int in_c; /* Prototypes */ int Init(); void fatalError(char * str, ...); SDL_Surface * imgLoad(char * fname); int initSurfaces(); int initSounds(); int loadFonts(); int txt(char * str, int c, int v, int h); /* Enums */ enum TxtColours{ RED, GREEN, BLUE, WHITE, BLACK }; int main(int argc, char *argv[]){ /* Inicializamos SDL y cargamos los gráficos */ if(!Init()) fatalError("Initalisation Failed\n\r"); /* Empecemos con la musiquita... */ if (use_sound) music_channel = Mix_PlayMusic(bgm, -1); /* Bucle principal */ do { if(SDL_GetTicks()-lastTock > speed){ lastTock = 0; } /* Dibujamos el fondo */ dest.x = 0; dest.y = 0; SDL_BlitSurface(background, NULL, scr_buffer, &dest); /* Mostramos el titulo */ zoom_factor = 0.5 + (cos(frame * 0.05) * 0.5); zoomed_title = zoomSurface(title, zoom_factor, zoom_factor, SMOOTHING_OFF); SDL_GetClipRect(zoomed_title, &ztitle_rect); xtitle = 240 - (ztitle_rect.w / 2); ytitle = 50 - (ztitle_rect.h / 2); title_dest.x = xtitle; title_dest.y = ytitle; SDL_BlitSurface(zoomed_title, NULL, scr_buffer, &title_dest); /* Dibujamos los créditos */ ttext = zoomSurface(text, zoom_credit, zoom_credit, SMOOTHING_OFF); SDL_GetClipRect(ttext, &dest); dest.x = 240 - (dest.w / 2); dest.y = 200 - (dest.h / 2); SDL_BlitSurface(ttext, NULL, scr_buffer, &dest); zoom_credit = zoom_credit + cz_factor; if ((zoom_credit <= 1.5)&&(cz_factor == -0.1)) { cz_factor = 0; } if (cz_factor == 0) { --c_wait; } if (c_wait <= 0) { cz_factor = -0.15; c_wait = 100; } if (zoom_credit <= 0.1) { c_wait = 100; cz_factor = -0.1; zoom_credit = 4; /* Siguiente credito */ in_c = (in_c + 1) % CREDITS; txt(credits[in_c], WHITE, 5, 5); } /* Dibujamos el buffer */ dest.x = 0; dest.y = 0; SDL_BlitSurface(scr_buffer, NULL, screen, &dest); SDL_Flip(screen); SDL_FreeSurface(zoomed_title); SDL_FreeSurface(ttext); ++frame; /* Estas rutinas no son necesarias en la PSP puesto */ /* que tenemos configurados los ExitCallbacks */ #ifndef PSP while(SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { done = 1; break; } } #endif } while(!done); /* Venga, hasta luego */ SDL_Quit(); #ifdef PSP sceKernelExitGame(); #endif exit(0); } int Init(){ /* Hilos de finalizacion */ #ifdef PSP SetupCallbacks(); #endif /* Inicializaciones globales */ done = 0; use_sound = 1; music_channel = 0; zoom_credit = 4; cz_factor = -0.1; c_wait = 100; frame = 0; lastTock = 0; /* Inicializacion de SDL */ if (SDL_Init(SDL_INIT_VIDEO) < 0) fatalError("No se pudo inicializar el sistema SDL\n\r"); /* Cargamos las fuentes TTF */ if(!loadFonts()) fatalError("No se pudo cargar la fuente\n\r"); /* Cargamos las imagenes */ if(!initSurfaces()) fatalError("No se pudieron cargar las imagenes\n\r"); /* Cargamos los sonidos */ if(!initSounds()) fatalError("No se pudieron cargar los sonidos\n\r"); /* Borramos la pantalla */ dest.x = 0; dest.y = 0; dest.w = 480; dest.h = 272; SDL_FillRect(screen, &dest, SDL_MapRGB(screen->format, 0, 0, 0)); dest.x = 0; dest.y = 0; SDL_BlitSurface(background, NULL, screen, &dest); credits[0] = "CRySoL SDL Demo"; credits[1] = "by Int-0"; credits[2] = "greetingzzz to"; credits[3] = "all CRySoL users"; credits[4] = "all Vigilando users"; credits[5] = "do you want some names?"; credits[6] = "ok... in no special order"; credits[7] = "clet-1"; credits[8] = "n4xer4s"; credits[9] = "kitty bang!"; credits[10] = "brue the one"; credits[11] = "modern girl"; credits[12] = "per if"; credits[13] = "oh..."; credits[14] = "special thanks to... "; credits[15] = "Dark_Alex"; credits[16] = "pspsdk stuff"; credits[17] = "ArCO stuff"; credits[18] = "...and.."; credits[19] = "anybody!! :)"; in_c = 0; txt(credits[in_c], WHITE, 5, 5); /* Draw init screen */ SDL_Flip(screen); return 1; } void fatalError(char * str, ...){ fprintf(stderr, "Error: %s: %s\n", str, SDL_GetError()); SDL_FreeSurface(zoomed_title); SDL_Quit(); #ifdef PSP sceKernelExitGame(); #endif exit(1); } /* Carga una imagen */ SDL_Surface * imgLoad(char * fname){ return IMG_Load(fname); } int initSurfaces(){ /* Creamos el layer de la pantalla, */ /* en la PSP es toda la pantalla y en PC será una ventana */ screen = SDL_SetVideoMode(480, 272, 16, SDL_HWSURFACE); if (screen == NULL) fatalError("SDL_SetVideoMode Error"); txt("Loading...", WHITE, 225, 132); SDL_Flip(screen); /* Cargamos el fondo en layer y en buffer */ background = imgLoad("background.png"); scr_buffer = imgLoad("background.png"); if (background == NULL) fatalError("Error cargando background.png"); title = imgLoad("crysol_title.png"); if (title == NULL) fatalError("Error cargando crysol_title.png"); return 1; } int initSounds(){ if (SDL_Init(SDL_INIT_AUDIO) < 0) use_sound = 0; if (use_sound) if (Mix_OpenAudio(44100, AUDIO_S16, 2, 512) < 0) fatalError("Error configurando la mezcladora"); if (use_sound){ bgm = Mix_LoadMUS("am-fm.mod"); if (bgm == NULL) fatalError("Error cargando am-fm.mod"); } } int loadFonts(){ if (TTF_Init() != 0) fatalError("Fallo al inicializar libreria TTF\r\n"); if (!(font = TTF_OpenFont("arial.ttf", 12))) fatalError("Error al abrir arial.ttf\r\n"); TTF_SetFontStyle(font, TTF_STYLE_NORMAL); return 1; } int txt(char * str, int colour, int v, int h){ SDL_Rect rect; SDL_Color c; switch(colour){ case 0: c.r = 255; c.g = 0; c.b = 0; break; case 1: c.r = 0; c.g = 255; c.b = 0; break; case 2: c.r = 0; c.g = 0; c.b = 255; break; case 3: c.r = 255; c.g = 255; c.b = 255; break; case 4: c.r = 0; c.g = 0; c.b = 0; break; } if(!(text = TTF_RenderText_Blended(font, str, c))) fatalError("Error renderizando fuente\r\n"); if(!(ttext = TTF_RenderText_Blended(font, str, c))) fatalError("Error renderizando fuente\r\n"); rect.x = v; rect.y = h; return 1; } #ifdef PSP /* Manejador del callback de salida */ int exit_callback(int arg1, int arg2, void *common) { done = 1; return 0; } /* Crea el callback de salida */ int CallbackThread(SceSize args, void *argp) { int cbid; cbid = sceKernelCreateCallback("Exit Callback", exit_callback, NULL); sceKernelRegisterExitCallback(cbid); sceKernelSleepThreadCB(); return 0; } /* Crea y configura el hilo para el callback y retorna su ID */ int SetupCallbacks(void) { int thid = 0; thid = sceKernelCreateThread("update_thread", CallbackThread, 0x11, 0xFA0, 0, 0); if(thid >= 0) { sceKernelStartThread(thid, 0, 0); } return thid; } #endifComo véis, toda la inicialización se ha metido en un método, que a su vez llama a varios métodos para incializar surfaces, sonidos y tipo de letra. Además hay un método encargado de mostrar mensajes de error en caso de producirse. Un apunte más: la función para renderizar texto tiene predefinidos 5 colores: rojo, verde, azul, blanco y negro.
Apéndice B: Compilar en Debian
Para poder compilar la demo debéis tener instalados los siguientes paquetes:- libsdl-dev
- libsdl-image1.2-dev
- libsdl-ttf2.0-dev
- libsdl-mixer1.2-dev
- libsdl-gfx1.2-dev
- libvorbisidec-dev
Enlaces
- Página oficial de libSDL.
- Recetas en CRySoL:
- Cortesía de vigilando:
[ show comments ]
blog comments powered by Disqus