Documentación de MiMotor Fernando Chamizo 14 de octubre de 2008 Índice 1. Información rápida 2 2. Parámetros 7 3. Variables del sistema 8 4. Variables globales 9 5. Imágenes 10 6. Sonido 10 7. Teclado y ratón 11 8. Interrupciones 11 9. Colisiones 12 10.Funcionamiento interno 13 11.Versiones y posibles extensiones 15 12.Más ejemplos 16 13.Apéndice: Instalar y compilar con SDL 22 1 MiMotor1.1 2 El rapidı́simo desarrollo de la informática y la dura competencia han hecho que se preste menos atención a la documentación: ¿para qué leer cientos de páginas del manual de uso de un programa si mañana, pasado o en unos meses tendré otro mejor? Esta tendencia ha tenido sus beneficios, principalmente que los desarrolladores han mirado más por el usuario general dotando a los programas de interfaces “para tontos” que no requieren apenas documentación. A menudo ya no hay fı́sicamente un libro de instrucciones sino información diseminada en unas cuantas páginas web o ayuda en lı́nea. Por supuesto hay también una parte negativa pero no entraremos aquı́ en ello. El motor gráfico que vamos a documentar es sinceramente malo, no hay que esperar a mañana o pasado para encontrar algo mejor sino que ya existen multitud de motores libres completı́simos que tienen nivel suficiente para desarrollar videojuegos profesionales actuales, mientras que con el nuestro sólo se llegarı́a a poco más del nivel de los años 80. Por ello hay que cuidar todavı́a más al posible lector y, además de escribir bien claras desde el principio la advertencia anterior, darle una primera seción de información rápida para que pueda saber a qué atenerse sin perder mucho tiempo. Después de la publicidad negativa ¿por qué alguien va a querer emplear este motor gráfico? Su uso es fundamentalmente académico y su gran ventaja la simplicidad. Es suficientemente breve para poder leer y entender todo el código y es multiplataforma. Además en la actualidad hay un gran revival de aquellos sencillos juegos de los 80 a través de los teléfonos celulares y aunque el lenguaje es diferente (Java en su versión J2ME en vez de C) todavı́a se pueden conservar algunas ideas fundamentales. Los requisitos son mı́nimos: el motor ha funcionado correctamente sin modificaciones en una reliquia a 135 Mhz. 1. Información rápida MiMotor es un minimotor gráfico multiplataforma basado en SDL enteramente escrito en C y extremadamente simple. La versión en curso es MiMotor1.1.c con fichero de cabecera MiMotor1.1.h. Un requisito básico para emplearlo es tener un conocimiento al menos somero de la biblioteca SDL (en http://www.loosersjuegos.com.ar/referencia/libros/libros.php se puede encontrar material completo), que debe estar instalada en la máquina en uso (véase §13) y naturalmente el lenguaje C se da por supuesto. Los comandos para activar y desactivar el motor son respectivamente motoron() y motoroff(). El programa (inoperante) más breve que lo emplea es: #include "MiMotor1.1.h" int main(int argc, char *argv[]){ motoron(); motoroff(); return EXIT_SUCCESS; } Para compilar este programa o cualquier de los sucesivos deben estar instaladas las bibliotecas de desarrollo de SDL (como se indica en §13). En Linux con gcc si el código anterior es programa.c y los ficheros MiMotor1.1.c y MiMotor1.1.h están en el mismo directorio el comando serı́a gcc programa.c MiMotor1.1.c -o mi ejecutable.out `sdl-config --cflags --libs` -lSDL image -lSDL mixer -lm. El motor utiliza una nomenclatura “orgánica” con la que a cada unidad básica, sprite, se le llama ser, a la imagen asociada cara y al conjunto de reglas que lo gobiernan cerebro. La función escena es la más importante, la única a través de la cual se pueden comunicar el motor gráfico con el programa que lo usa. Contiene al bucle principal y conecta el motor con MiMotor1.1 3 las rutinas donde se inician los seres, se gestionan las interrupciones y se tratan las colisiones. Su prototipo es el siguiente: void escena( void (*creaseres)(void), void (*leetec)(void), Uint32 (*interrup)(struct Ente *ser,Uint32 ahora), void (*colision)(struct Ente *ser) ); /* /* /* /* inicio de seres */ lectura del teclado */ gestión de interrupciones */ gestión de colisiones */ Consideremos en primer lugar un ejemplo en el que no se lee el teclado, no hay colisiones y la gestión de interrupciones es una simple rutina por defecto. Todo esto se indica asignando NULL a los tres últimos argumentos. Pretendemos mostrar una bola que se mueva horizontalmente en la pantalla rebotando en los bordes y para ello usaremos escena(creaunabolita,NULL,NULL,NULL). En la rutina creaunabolita cargaremos una imagen (la de la bolita) y asignaremos a un ser ciertos parámetros, entre ellos la imagen. Empleamos: void creaunabolita(void){ cargaspri("./images/bolita.png",0,NULL); /* Carga imagen */ // nace(tipo,cont,(x,y),(vx,vy),cerebro,cara) nace(0,200,320.0,400.0,9.0,0.0,c_rebota,album[0]); } La primera función, cargaspri, sirve para cargar la imagen indicada como primer argumento (en este caso bolita.png que está en el directorio images, los formatos .gif, .jpg y .bmp también son válidos) y almacenarla en el primer hueco libre de la matriz de imágenes album[]. Más adelante veremos que los otros dos argumentos permiten cargar diferentes fotogramas. Los parámetros del ser se podrı́an asignar directamente a los miembros de la estructura Ente a la que responde cada ser pero es más sencillo emplear nace. En la lı́nea comentada se indica el significado de cada argumento. Los dos primeros son enteros de utilidad general y en principio el primero está pensado para indicar el tipo de ser o sus propiedades y el segundo como contador. Los cuatro siguientes son float que indican la posición en la pantalla y la velocidad, aunque la velocidad puede usarse para otros fines. El siguiente apunta a una función, el cerebro, que indica el comportamiento del ser y el último es la cara, la imagen asignada al ser. Con nuestra instrucción tendremos un ser que inicialmente tiene su contador a 200, y está en la posición (320.0,400.0) con velocidad (9.0,0.0). A continuación debemos definir el cerebro correspondiente de este ser que viene dado por la función c_rebota void c_rebota(Ente *ser){ ser->xa=ser->x; /* Actualiza coordenada */ ser->x+=ser->vx; /* A~ nade la velocidad */ if(ser->x>ANCHO-20){ ser->x=ANCHO-20; ser->vx*=-1;} /* Rebotes */ if(ser->x<0){ ser->x=0; ser->vx*=-1;} if(!--ser->cont) V2_FLAGSS|=FESC; /* Termina */ } La primera lı́nea asigna a la coordenada x anterior, ser->xa, la de la última posición, ser->x, que va a ser modificada. Esta actualización de las coordenadas antiguas es necesaria siempre que vayan a sufrir cambios. La siguiente lı́nea cambia la posición añadiendo la velocidad. Si estamos cerca del borde la velocidad cambia de signo, lo que produce el rebote. En MiMotor1.1.h están definidas las constantes ANCHO y ALTO que dan el ancho y el alto de la pantalla como 640 y 480 pixels. El 20 que se resta proviene de que la imagen bolita.png es de 16 × 16 pixels. Finalmente si el contador del ser llega a cero entonces en, V2 FLAGSS se activa el bit que corresponde al flag de escape. La rutina por defecto de gestión de interrupciones lo único que hace es salir cuando este bit está activado. Si queremos emplear una imagen de diferente tamaño podemos escribir en la primera de las lı́neas que gestionan los rebotes if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; ser->vx*=-1;} /* Rebotes */ MiMotor1.1 4 porque ser->cara es la cara (la imagen) del ser y ser->cara->w es su ancho (width), mientras que ser->cara->h es su alto (height). En total el programa serı́a: /* /ficheros/rebotes.c */ #include "MiMotor1.1.h" #define DIRECI "./images/" #define DIRECS "./sounds/" /* Directorio de imágenes */ /* Directorio de sonidos */ void c_rebota(Ente *ser) { ser->xa=ser->x; /* Actualiza coordenada */ ser->x+=ser->vx; /* A~ nade la velocidad */ /* Rebotes */ if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; ser->vx*=-1;} if(ser->x<0){ ser->x=0; ser->vx*=-1;} if(!--ser->cont) V2_FLAGSS|=FESC; /* Termina */ } void creaunabolita(void){ cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ // nace(tipo,cont,(x,y),(vx,vy),cerebro,cara) nace(0,200,320.0,400.0,9.0,0.0,c_rebota,album[0]); } int main(int argc, char *argv[]){ motoron(); escena(creaunabolita,NULL,NULL,NULL); motoroff(); return EXIT_SUCCESS; } Modificando el cerebro c rebota el comportamiento será otro. Por ejemplo, si quisiéramos que el movimiento fuera oblicuo rebotando en todas las paredes el nuevo cerebro serı́a: void c_rebota(Ente *ser) { ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenadas */ ser->x+=ser->vx; ser->y+=ser->vy; /* A~ nade la velocidad */ /* Rebotes */ if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; ser->vx*=-1;} if(ser->x<0){ ser->x=0; ser->vx*=-1;} if(ser->y>ALTO-ser->cara->h){ ser->y=ALTO-ser->cara->h; ser->vy*=-1;} if(ser->y<0){ ser->y=0; ser->vy*=-1;} if(!--ser->cont) V2_FLAGSS|=FESC; /* Termina */ } Cambiemos también la llamada a nace a nace(0,400,320.0,400.0,9.0,7.0,c_rebota,album[0]) duplicando la duración y añadiendo una velocidad vertical. Aproximadamente cada cerebro funciona FPSM veces por segundo donde FPSM está definido en MiMotor1.1.h como 20, entonces un valor inicial de n para el contador equivaldrá a una duración aproximada de n/20 segundos. Si ahora quisiéramos una bandada de 40 bolitas con posiciones iniciales aleatorias y velocidad 5.0 en direcciones aleatorias deberı́amos cambiar creaunabolita por ejemplo de la siguiente forma: void creaunabolita(void){ int i=40; MiMotor1.1 // 5 float th; cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ while(i--){ th=M_PI*(rand() % 20)/10.0; nace(tipo,cont,(x,y),(vx,vy),cerebro,cara) nace(0, 400, rand() % (ANCHO-album[0]->w), rand() % (ALTO-album[0]->h), 5.0*cos(th), 5.0*sin(th),c_rebota,album[0]); } } El número máximo de seres por defecto es 128 pero se puede modificar cambiando el valor de la constante NMAXENTES en MiMotor1.1.h. Importante: Forzando la velocidad de los seres, el tamaño de sus caras o su número la animación empeora. Es el precio que hay que pagar por emplear un motor tan simple. Una imagen 48 × 48 a 200 pixels por segundo pierde bastante nitidez y a 400 pixels por segundo es difı́cilmente usable, por otro lado a 100 pixels por segundo sólo se nota una pequeña vibración. SDL es una biblioteca muy eficiente para imprimir imágenes rápido en la pantalla pero la actualización para que realmente se vean esas imágenes y la dificultad para sincronizar lleva a problemas con las animaciones rápidas o de imágenes grandes. Ahora partiremos del programa anterior con las modificaciones indicadas incluyendo las 40 bolitas, y vamos a dotarle de las rutinas para leer el teclado, gestionar las interrupciones y gestionar las colisiones que antes habı́amos indicado con NULL en los argumentos de escena. Veamos primero la lectura del teclado. El propio motor incluye alguna función que permite leer cierto número de teclas almacenando el resultado en la variable del sistema V2_ESTTEC. Para practicar con SDL aquı́ crearemos nuestro propio código pero todavı́a usando esta variable para almacenar el resultado. La instrucción SDL_PollEvent(&event) analiza si hay algún evento (cambio en el ratón, teclado, etc.) y en su caso almacena sus propiedades en su argumento, aquı́ en un puntero a una estructura de tipo SDL_Event. La forma en que esta estructura codifica la información es un poco larga de explicar. Aquı́ sólo utilizaremos el miembro type para saber si se ha pulsado una tecla, SDL_KEYDOWN, y la función SDL_GetKeyState(NULL) que apunta a una matriz que en la posición SDLK_[tecla] tiene un valor (de tipo Uint8) nulo si la tecla no está pulsada. El siguiente ejemplo permite salir del programa si se pulsa la tecla Esc y alterna los valores 1 y 0 para V2_ESTTEC cuando se pulsa la barra espaciadora. void espacioyescape(void){ Uint8 *keystate = SDL_GetKeyState(NULL); SDL_Event event; /* Si hay eventos */ if(SDL_PollEvent(&event)){ if((event.type==SDL_KEYDOWN)&&(keystate[SDLK_SPACE])) /* espacio => flip */ V2_ESTTEC=1- V2_ESTTEC; if((event.type==SDL_KEYDOWN)&&(keystate[SDLK_ESCAPE])) /* escape => sale */ V2_FLAGSS|=FESC; /* Activa flag de escape */ } } Debemos modificar consecuentemente la llamada a escena añadiendo el nuevo argumento, y en el cerebro cambiaremos el signo de la velocidad si V2_ESTTEC es 1, también podemos suprimir if(!--ser->cont) V2_FLAGSS|=FESC; para que el programa funcione indefinidamente hasta que se pulse Esc. El nuevo listado completo será: escena(creaunabolita,espacioyescape,NULL,NULL); MiMotor1.1 6 /* /ficheros/rebmlee.c */ #include "MiMotor1.1.h" #define DIRECI "./images/" #define DIRECS "./sounds/" /* Directorio de imágenes */ /* Directorio de sonidos */ void c_rebota(Ente *ser) { ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenada */ /* Velocidad con signo */ ser->x+=(1-2*V2_ESTTEC)*ser->vx; ser->y+=(1-2*V2_ESTTEC)*ser->vy; /* Rebotes */ if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; ser->vx*=-1;} if(ser->x<0){ ser->x=0; ser->vx*=-1;} if(ser->y>ALTO-ser->cara->w){ ser->y=ALTO-ser->cara->h; ser->vy*=-1;} if(ser->y<0){ ser->y=0; ser->vy*=-1;} if(!--ser->cont) V2_FLAGSS|=FESC; /* Termina */ } void creaunabolita(void){ int i=40; float th; cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ while(i--){ th=M_PI*(rand() % 20)/10.0; // nace(tipo,cont,(x,y),(vx,vy),cerebro,cara) nace(0, 400, rand() % (ANCHO-album[0]->w), rand() % (ALTO-album[0]->h), 5.0*cos(th), 5.0*sin(th),c_rebota,album[0]); } } /************/ /* LEETEC */ /************/ void espacioyescape(void){ Uint8 *keystate = SDL_GetKeyState(NULL); SDL_Event event; /* Si hay eventos */ if(SDL_PollEvent(&event)){ /* Termina si se cierra la ventana */ if((event.type==SDL_KEYDOWN)&&(keystate[SDLK_SPACE])) V2_ESTTEC=1- V2_ESTTEC; if((event.type==SDL_KEYDOWN)&&(keystate[SDLK_ESCAPE])) V2_FLAGSS|=FESC; /* Activa flag de escape */ } /* espacio => flip */ /* escape => sale */ } int main(int argc, char *argv[]){ motoron(); escena(creaunabolita,espacioyescape,NULL,NULL); motoroff(); return EXIT_SUCCESS; } El tercer argumento de escena es una función que gestiona las interrupciones cuyo prototipo es Uint32funcion(Ente*ser,Uint32ahora). El primer argumento se emplea si fuera necesario efectuar manipulaciones con los seres, mientras que el segundo indica el tiempo al entrar en la función. No vamos a utilizar ninguno por ahora. Lo fundamental es saber que un cero a la MiMotor1.1 7 salida implica una salida de escena y que la salida tı́pica es el tiempo de entrada. Para hacer algo original, pintaremos de rojo la pantalla antes de salir con Esc, para ello emplearemos: Uint32 escapa(Ente *ser,Uint32 ahora){ /* Gestión de escape */ if( V2_FLAGSS&FESC ){ SDL_FillRect(screen,NULL,ROJO); SDL_Flip(screen); SDL_Delay(1000); /* Espera 1s */ return 0; /* Sale de escena */ } return ahora; /* Por ahora innecesario */ } conectado a escena con escena(creaunabolita,espacioyescape,escapa,NULL); y habiendo definido con #define ROJO SDL_MapRGB(screen->format,223,47,47) que es la manera de escribir en SDL el color RGB=(223,47,47) en el formato usado en la pantalla. El comando SDL_FillRect(screen, NULL,ROJO) se explica por sı́ solo, SDL_Flip(screen) actualiza la pantalla y SDL_Delay(1000) espera 1 s=1000 ms (el argumento de SDL_Delay tiene una imprecisión, “granularidad”, de unos 10 ms). Finalmente vamos a gestionar las colisiones. Para ello cambiemos un poco creaunabolita sustituyendo la lı́nea cargspri por ROJO cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ cargaspri(DIRECI "bolitaa.png",0,NULL); /* Carga imagen */ cargaspri(DIRECI "bolitar.png",0,NULL); /* Carga imagen */ nace(0,0, 0.0,0.0, 6.39,4.79, c_rebota, album[1]); donde bolitaa.png y bolitar.png son las imágenes de una bolita azul y otra roja que se almacenarán en album[1] y album[2]. Queremos que cuando la única bolita azul (el ser que se crea con el nace anterior) choque con alguna verde la convierta en roja. En el motor al primer ser de la lista se le llama adan. Una posible rutina de colisión con este cometido es la siguiente, que debe ponerse como último argumento de escena: void chocaroja(Ente *ser){ float vx=adan->x - ser->x,vy=adan->y - ser->y; /* Radiovector */ /* Si es verde y está cerca */ if( ( ser->cara==album[0] ) && (vx*vx+vy*vy <16*16) ) ser->cara= album[2]; } La constante 16 ∗ 16 es el cuadrado de la distancia para que se contabilice una colisión. Si se quisiera variar el tamaño de las bolitas el valor exacto para que se produzca la colisión serı́a el cuadrado de (album[0]->w + album[1]->w)/2. Si el tipo de un ser tiene el bit de mayor peso activado entonces desaparece inmediatamente. En la cabecera MUER está definido como la máscara 32768 = 215 . Por ejemplo, cambiando el condicional de chocaroja por if( ( ser->cara==album[0] ) && (vx*vx+vy*vy <16*16) ) ser->tipo|=MUER; 2. Parámetros El fichero de cabecera MiMotor1.1.h comienza cargando diversas bibliotecas con las siguientes lı́neas: #ifdef HAVE_CONFIG_H #include <config.h> #endif #include <SDL/SDL.h> #include <SDL/SDL_image.h> #include <SDL/SDL_mixer.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> MiMotor1.1 8 A continuación hay una definición de diferentes parámetros modificables que permiten ajustar el motor: /* Formato de pantalla */ #define ANCHO 640 /* Ancho de pantalla */ #define ALTO 480 /* Alto de pantalla */ #define BPP 4 /* Bytes por pixel */ /* Compilación */ #define PROBANDO 0 /* Modo de prueba */ /* Definiciones del motor */ #define DESCANSO 0 /* Milisegundos de descanso de la CPU */ #define FPSM 20 /* Frames lógicas por segundo divididas por 1.024 (20 ¿mejor 30?)*/ #define NMAXENTES 128 /* Número máximo de seres */ #define NMAXIMAG 128 /* Número máximo de imágenes */ #define NMAXSON 64 /* Número máximo de sonidos */ Todos quedan explicados excepto quizá PROBANDO, que cuando está activado a 1 da información adicional acerca de la carga de imágenes y sonidos. También en la cabecera está definida la estructura principal Ente typedef struct Ente { float xa,ya; /* Coordenadas antiguas */ float x,y; /* Coordenadas actuales */ float xi,yi; /* Coordenadas interpoladas */ float vx,vy; /* Vector velocidad */ Uint16 tipo; /* Tipo de ser (y flags) */ Uint16 cont; /* Contador de propósito general */ SDL_Rect pisada; /* Rectángulo donde se imprime */ void (*cerebro)(struct Ente *ser); /* Cerebro */ SDL_Surface *cara; /* Imagen */ } Ente; que representa un sprite básico regulado por una función cerebro, lo que llamamos un ser. 3. Variables del sistema Los parámetros variables del motor no son miembros de una gran estructura, como serı́a natural, sino que se almacenan en direcciones de memoria consecutivas. Constituyen las variables del sistema (un tributo a los tiempos del Spectrum). Hay de dos tipos, de 2 bytes y de 4 bytes distribuidos en dos tablas. Los inicios de cada tabla son variables globales Uint16 *varsis2; /* Las variables del sistema de 16 bits */ Uint32 *varsis4; /* Las variables del sistema de 32 bits */ asignadas con malloc para que se reserve espacio. varsis2=(Uint16*)calloc(NUMVS2,2); varsis4=(Uint32*)calloc(NUMVS4,4); Se accede al valor de una variable del sistema mediante V2_nombre o V4_nombre. La definición interna de V2_nombre es en general *(varsis2+nombre) y lo mismo para V4_nombre. Es decir, nombre indica el desplazamiento respecto a las variables globales anteriores. Las excepciones son FOTOS y TECLAS, definidas como punteros a 16 y a 10 posiciones libres de tipo Uint16, respectivamente, que sirven en el primer caso para marcar posiciones del almacén de imágenes album[], y en el segundo para indicar el comienzo de la lista de teclas definidas. Las constantes NUMVS2 y NUMVS4 indican el número de posiciones de memoria ocupadas por variables del sistema de dos y cuatro bytes, es decir (contando FOTOS por 16 y TECLAS por 10) el número de variables del sistema. Por ahora NUMVS2 es 33 y NUMVS4 es 21. Las variables del sistema de 16 bits son: MiMotor1.1 9 Definición V2_NSERAC V2_ESTTEC TECLAS V2_FLAGSS V2_NVIDAS V2_NITEMS V2_MUNICI V2_NNIVEL FOTOS Posiciones 0 1 2–11 12 13 14 15 16 17–33 Descripción NÚMERO DE SERES ACTIVOS ESTADO DEL TECLADO TECLAS DEFINIDAS FLAGS DEL SISTEMA NÚMERO DE VIDAS NÚMERO DE ITEMS RESERVA DE MUNICIÓN NÚMERO DE NIVEL MARCADOR DE IMÁGENES EN album[] Los nombres son indicativos y pueden usarse con otros propósitos, excepto V2_NSERAC y V2_FLAGSS que se usan internamente. Las variables del sistema de 32 bits son: Definición V4_TLOGIC V4_NFRLOG V4_PUNTUA V4_MARCAD V4_CFONDO V4_ALMACXn V4_ALMACYn Posiciones 0 1 2 3 4 5–12 13–20 Descripción TIEMPO LÓGICO NÚMERO DE FRAMES LÓGICAS PUNTUACIÓN NÚMERO EN EL MARCADOR COLOR DE FONDO ALMACEN DE 8 VALORES ’X’ ALMACEN DE 8 VALORES ’Y’ Internamente se usan V4_TLOGIC, V4_NFRLOG y V4_CFONDO. La notación de las últimas variables indica que éstas son V4_ALMACX0,. . . V4_ALMACX7 y V4_ALMACY0,. . . V4_ALMACY7. La variable V2_FLAGSS almacena los flags del sistema utilizados en las interrupciones. Por ahora sólo hay cuatro. Su valor indica la máscara del bit que se activa. Con la estructura actual del bucle principal sólo los 8 bits de menor peso de V2_FLAGSS activan la gestión de interrupciones. FPAU=1. Modo pausa. Lo gestiona la interrupción correspondiente y devuelve SDL_GetTicks() para engañar al bucle principal señalado que no ha transcurrido tiempo. Esta técnica se sigue en todos los flags excepto para FESC. FACT=2. Indica que se debe hacer alguna actualización especial (por ejemplo el marcador). FRIP=64. Muerte del personaje principal. Tı́picamente la interrupción ejecuta una “danza final”, disminuye V2_NVIDAS y en caso de que sea cero activa FESC. FESC=128. Escape. Provoca una interrumpción inmediata saliendo del bucle principal. La interrupción debiera tratarlo devolviendo inmediatamente el valor cero. Con ello se saldrá del bucle principal. 4. Variables globales Aparte de varsis2 y de varsis4 hay otras seis variables globales: Ente *adan; /* Primer ser */ SDL_Surface *album[NMAXIMAG+1]; /* Álbum de imágenes */ SDL_Surface *fondo; /* Fondo de pantalla */ Mix_Chunk *gramola[NMAXSON+1]; /* Álbum de sonidos */ void *mapa; /* Origen de datos utilizables por el motor */ SDL_Surface *screen; /* La pantalla */ MiMotor1.1 10 La más importante es la pantalla, screen. Las matrices album[] y gramola[] almacenan las imágenes y sonidos que se van a utilizar en escena. A veces es conveniente saber la dirección del primer ser que se ha creado, por ejemplo para averiguar el orden del ser en curso, éste es el propósito del puntero adan. La superficie fondo es diferente de NULL sólo cuando se quiere establecer una imagen de fondo. Por último el puntero mapa está pensado con el propósito de apuntar a cualquier tipo de colección de datos que se quierea emplear en escena. 5. Imágenes La función básica para cargar una imagen es cargaspri. Como hemos visto, cargaspri( hace que la primera posición de album[] apunte a la imagen cargada desde el fichero "nombre". La función cargaspri llama a cargimag cuya prototipo es SDL_Surface *cargimag(char *fichero) y que devuelve un puntero a la imagen cargado desde fichero. A veces un mismo fichero contiene n fotogramas de un sprite. En ese caso utilizaremos cargaspri("nombre",n,xy) para que n posiciones de album[] apunten a las n imágenes. Respecto al último argumento, xy[] es una matriz de 2n enteros que indican las coordenadas de la esquina inferior derecha de cada uno de los fotogramas. Por ejemplo, en el programa al comienzo de esta documentación, si empleamos el fichero bolita4.png donde se alternan dos bolitas de tamaño 16 con otras un 25 % mayor y un 25 % menor, las cargarı́amos en creaunabolita mediante "nombre",0,NULL) int xy[2*4]={16,16,16+20,20,36+26,16,52+12,12}; cargaspri(DIRECI "bolita4.png",4,xy); Si al comienzo de c_rebota incluimos ser->cara=album[((int)ser->x/27)%4]; entonces la bolita va latiendo según avanza hinchándose y deshinchándose. Cuando se tienen varios grupos de imágenes cargadas en album[] no es fácil recordar la posición de cada una de ellas. La matriz entera FOTOS[] en conjunción con la función nimenal() que da el número de imágenes ya cargadas en album[] es útil para establecer marcadores. Por ejemplo FOTOS[i]=nimenal(); cargaspri("nombre",n,xy); permite referirnos con album[FOTOS[i]] al puntero del primer fotograma de "nombre" sin necesidad de recordar cuántas posiciones de album[] se han ocupado con imágenes anteriores. 6. Sonido La carga de sonidos contenidos en los ficheros fichero0.wav, fichero1.wav,. . . se lleva a cabo con char sonidos[]= "fichero0.wav#fichero0.wav#fichero0.wav#...ficheroN.wav"; cargason(sonidos); Los punteros a los sonidos quedan almacenados en gramola[0], gramola[1], etc. Para hacerlos sonar se utiliza la instrucción Mix_PlayChannel. El significado de sus tres argumentos está en la documentación de la biblioteca SDL_mixer. Por regla general, para ejecutar el sonido al que apunta gramola[i] se empleará Mix_PlayChannel(-1,gramola[i],0). Por ejemplo, si en el programa inicial dentro de creaunabolita cargamos blip.wav con char sonidos[]= "blip.wav"; cargason(sonidos); y en c_rebota añadimos MiMotor1.1 11 if( (ser->x-ANCHO/2)*(ser->xa-ANCHO/2)<0 ) Mix_PlayChannel (-1,gramola[0],0); entonces sonará blip.wav cada vez que la bolita pase por el punto medio. Cuando se sale de escena se llama a la función liberaimagyson() que libera todo el espacio ocupado por las imágenes y sonidos a los que apuntan los elementos de album[] y gramola[]. Lo mismo se hace con la imagen a la que apunta fondo, si es que hubiera un fondo definido. 7. Teclado y ratón El segundo argumento de escena es una función de tipo void sin argumentos que tı́picamente se utiliza para cargar en la variable V2_ESTTEC las acciones sobre el teclado y el ratón. En el fichero de cabecera están predefinidas las máscaras TARR, TABA, TIZQ, TDCH y TSAL como 1, 2, 4, 8 y 16 respectivamente que son coherentes con la función cincotec incluida en el motor, la cual lee las teclas cuyo código SDL (en general coincide con el ASCII) está almacenado en TECLAS[2], TECLAS[3], TECLAS[1], TECLAS[0], TECLAS[4] y activa los bits correspondientes de V2_ESTTEC correspondientes a las máscaras anteriores. También activa el flag FPAU si la tecla de Pausa (o F12) está pulsada, y sale del motor si se cierra la ventana o se pulsa F7. Por ejemplo, si antes de llamar a escena se definen las teclas como TECLAS[2]=SDLK_UP; TECLAS[3]=SDLK_DOWN; TECLAS[1]=SDLK_LEFT; TECLAS[0]=SDLK_RIGHT; TECLAS[4]=SDLK_SPACE; entonces utilizando cincotec como argumento de escena, con el siguiente cerebro se moverá con los cursores un ser por la pantalla de 8 en 8 pixels y la barra espaciadora permite abandonar. void c_rebota(Ente *ser) { ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenada */ if(V2_ESTTEC&TARR) if(V2_ESTTEC&TABA) if(V2_ESTTEC&TIZQ) if(V2_ESTTEC&TDCH) ser->y-= ser->y+= ser->x-= ser->x+= 8.0; 8.0; 8.0; 8.0; if(ser->y<0){ ser->y=0; } if(ser->y>ALTO-ser->cara->h){ ser->y=ALTO-ser->cara->h; } if(ser->x<0){ ser->x=0; } if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; } if(V2_ESTTEC&TSAL) V2_FLAGSS|=FESC; /* Termina */ } Ya hemos visto en la información rápida cómo leer el teclado con la biblioteca SDL. El siguiente ejemplo asigna V2_ESTTEC=1 cuando está pulsado el botón del ratón y en ese caso asigna a las variables V4_ALMACX0 y V4_ALMACY0 las coordenadas del cursor. void leeraton(void){ SDL_Event event; V2_ESTTEC= 0; if(!SDL_PollEvent(&event)||(event.type!=SDL_MOUSEBUTTONDOWN)) return; V2_ESTTEC= 1; V4_ALMACX0= event.button.x; V4_ALMACY0= event.button.y; } 8. Interrupciones El tercer argumento de escena es una función que se ajusta al prototipo MiMotor1.1 12 Uint32 nombre_interrup(Ente *ser, Uint32 ahora); La variable ahora contiene el tiempo en el que se accede a la función. Si la gestión de interrupciones conlleva una duración inapreciable su valor será también la cantidad de salida pero en otro caso, y especialmente en la gestión de FPAU, como ya hemos indicado se devuelve SDL_GetTicks() para que el bucle principal actúe como si no hubiera pasado el tiempo. El siguiente ejemplo gestiona la pausa esperando la pulsación de la tecla Intro o Escape; en el primer caso continúa como ha entrado y en el segundo caso interrumpe con FESC /* Auxiliar */ char escapa(void){ /* Vuelve 1 si se ha pulsado enter o esc, cero en otro caso */ Uint8 *keystate = SDL_GetKeyState(NULL); SDL_Event event; /* Si no hay tecla accionada, vuelve con cero*/ if(SDL_PollEvent(&event)){ if( event.type==SDL_QUIT ) /* Fin si cierra ventana */ exit(EXIT_FAILURE); if((event.type!=SDL_KEYDOWN)&&(event.type!=SDL_KEYUP)) if(keystate[SDLK_ESCAPE]){ /* Sale */ V2_FLAGSS|=FESC; return 1; /* Escape */ } if(keystate[SDLK_RETURN]) return 1; /* Pausa */ } return 0; return 0; } Uint32 pausayescape(Ente *ser, Uint32 ahora){ if(V2_FLAGSS&FPAU){ while(!escapa()); V2_FLAGSS^=FPAU; /* Lo quita */ return SDL_GetTicks(); } if(V2_FLAGSS&FESC) return 0; return ahora; } 9. Colisiones El último argumento de escena, cuando no es NULL, es una función de cada ser que trata las colisiones. Tı́picamente una colisión conlleva una activación de la máscara MUER en ser->tipo o un cambio en la velocidad. El siguiente código es una función colisión que incorporada al programa de las 40 bolas que rebotan provoca que al chocar salgan a velocidad 5.0 en sentidos opuestos en la dirección del vector que los une. void chocarebota(Ente *ser){ int i; int nser=ser-adan; /* Número de ser */ float vx,vy; /* Radiovector */ float t; for(i=1; i<V2_NSERAC-nser; ++i){ /* Estudia si ser y ser+i se han chocado */ vx=(ser+i)->x - ser->x; vy=(ser+i)->y - ser->y; t=vx*vx+vy*vy; /* Norma al cuadrado */ if( t<16*16 ){ MiMotor1.1 13 if( (t=sqrt(t)) == 0 ) t=vx=1.0; /* Por si coinciden */ t/=5.0; /* Direcciones opuestas al radiovector */ ser->vx= -vx/t; ser->vy= -vy/t; (ser+i)->vx= vx/t; (ser+i)->vy= vy/t; return; /* Impide colisión múltiple */ } } } 10. Funcionamiento interno El motor utiliza “ritmo lógico constante” (fixed rate logic), es decir, intenta que la lógica se ejecute un número prefijado de veces por segundo, que está definido por el parámetro FPSM. La ventaja frente a la técnica de ∆-tiempos es que se tiene control total sobre las velocidades y posiciones intermedias de un sprite. La pantalla se compone usando “rectángulos sucios” (dirty rectangles): sólo se actualizan los rectángulos correspondientes a la posición de los seres. Para calcular las posiciones se emplea interpolación lineal. Todas estas ideas están en el juego pig-1.0 de D. Olofson http://olofson.net/mixed.html. El bucle principal, casi todo el contenido de escena, es el siguiente: Ente ser[NMAXENTES]; SDL_Rect rastro[NMAXENTES]; Uint32 ahora,antes; /* Variables auxiliares de tiempo */ Uint32 i; /* Variable auxiliar */ adan=ser; /* apunta al primer ser */ fondo=NULL; /* Para posibles rutinas que comprueben si hay fondo */ /* Aquı́ el fondo debe estar liberado */ V2_FLAGSS&=0xFF00; /* Quita los flags locales */ if(interrup==NULL) interrup=fescape; /* Interrupción por defecto */ creaseres(); /* BUCLE PRINCIPAL */ SDL_Delay(10); /* Para que no sea el tiempo inicial nulo */ antes=SDL_GetTicks(); /* Inicio de tiempo */ logica(ser,colision); V4_TLOGIC=0; while(antes){ ahora=SDL_GetTicks(); i=-(V4_TLOGIC>>10); V4_TLOGIC+=(ahora-antes)*FPSM; i+=(V4_TLOGIC>>10); while(i--) logica(ser,colision); borpinact(ser,rastro); /* Borra, pinta, actualiza */ /* Lee teclado */ if( leetec!=NULL ) leetec(); #if DESCANSO!=0 SDL_Delay(DESCANSO); #endif if( V2_FLAGSS&255 ) ahora=interrup(ser,ahora); antes=ahora; /* Actualiza tiempo */ } Y la rutina borpinact que borra pinta y actualiza es: void borpinact(Ente *ser,SDL_Rect *rastro) { MiMotor1.1 /* Parámetro de interpolación */ float h=(float)(V4_TLOGIC&0x3ff)/1024.0; Uint16 j=0; Uint16 i=V2_NSERAC; /* BORRA */ if(fondo!=NULL) while(i--){ *rastro=(ser++)->pisada; /* Lo imprimido antes */ SDL_BlitSurface(fondo,rastro,screen,rastro); ++rastro; } else while(i--){ *rastro=(ser++)->pisada; /* Lo imprimido antes */ SDL_FillRect(screen,rastro++,V4_CFONDO); } /* PINTA */ i=V2_NSERAC; while(i--){ --rastro; --ser; /* Siguientes rastro y ser */ if((ser->tipo&MUER)&&!j){ /* Salta el primer muerto */ /* Parche para que el primero pueda morir y se acabe todo */ if(!i){ /* Si es el primero */ SDL_UpdateRects(screen, V2_NSERAC,rastro); /* Actualiza */ V2_FLAGSS|=FESC; /* Y sale con flag de escape */ return; } j=i--; --rastro; --ser; } /* Interpola coordenadas */ ser->xi=(1.0-h)*ser->xa+h*ser->x; ser->yi=(1.0-h)*ser->ya+h*ser->y; /* Lo que se imprime ahora */ ser->pisada.x=ser->xi; ser->pisada.y=ser->yi; SDL_BlitSurface(ser->cara, NULL,screen,&(ser->pisada)); /* Calcula rectángulo unión de ser->rastro */ if(rastro->x > ser->pisada.x){ if(rastro->x+rastro->w < ser->pisada.x+ser->pisada.w) rastro->w=ser->pisada.w; else rastro->w+=rastro->x-ser->pisada.x; rastro->x=ser->pisada.x; } else if(rastro->x+rastro->w<ser->pisada.x+ser->pisada.w) rastro->w=ser->pisada.x-rastro->x+ser->pisada.w; if(rastro->y > ser->pisada.y){ if(rastro->y+rastro->h < ser->pisada.y+ser->pisada.h) rastro->h=ser->pisada.h; else rastro->h+=rastro->y-ser->pisada.y; rastro->y=ser->pisada.y; } else if(rastro->y+rastro->h<ser->pisada.y+ser->pisada.h) rastro->h=ser->pisada.y-rastro->y+ser->pisada.h; } /* ACTUALIZA */ SDL_UpdateRects(screen, V2_NSERAC,rastro); if(j){ /* Si habı́a algún muerto suprimirlo */ ser[j]=ser[--V2_NSERAC]; /* Sustituye último */ rastro[j]=rastro[V2_NSERAC]; j=0; /* Ya no hay muerto */ } 14 MiMotor1.1 15 } El motor tiene una pequeña rutina merror que se usa internamente para gestionar algunos errores. También está disponible para el usuario. Su código es: void merror(int nerr,char* fich,int lin, char* mens, char* arg){ printf("Error en el fichero %s, lı́nea %d.\n",fich,lin); if(arg==NULL) printf("%s.\n",mens); else printf("%s%s\n",mens,arg); if(nerr<4){ printf("Éste es un error fatal.\n"); exit(EXIT_FAILURE); } } y las definiciones incluidas en la cabecera son: #define #define #define #define #define #define #define #define #define #define #define E_VIDEO 0,__FILE__,__LINE__,SDL_GetError(),NULL E_SCREE 1,__FILE__,__LINE__,SDL_GetError(),NULL E_BPERP 2,__FILE__,__LINE__,"Número de bytes por pixel no válido",NULL E_CAIMA 3,__FILE__,__LINE__,"Error al cargar la imagen " E_CASON 4,__FILE__,__LINE__,"Error al cargar el sonido " E_AUDIO 5,__FILE__,__LINE__,Mix_GetError(),". Problemas con el sonido." E_MUIMA 6,__FILE__,__LINE__,"Demasiadas imágenes en album[]",NULL E_MUSON 7,__FILE__,__LINE__,"Demasiados sonidos en gramola[]",NULL E_MUSER 8,__FILE__,__LINE__,"Demasiados seres",NULL E_GENER 9,__FILE__,__LINE__ E_DATNUM ,__FILE__,__LINE__,NULL, Por ejemplo merror(E_MUSER) aparece en nace cuando se intenta crear el ser número NMAXENTES y merror(E_CASON,nombre) aparece en cargason cuando hay problemas para cargar el fichero de sonido nombre. 11. Versiones y posibles extensiones es el motor del juego “Traspapelon” que se puede descargar en http://www.uam. salvo un pequeño cambio (la función fescape que establece la interrupción por defecto). Hasta que se acabe la documentación no se prevé hacer cambios en MiMotor1.1.c. MiMotor1.0.c es/fernando.chamizo/infor/infor.html Posibilidades TODO: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. Todas las variables Uint32 Más flags para actualización FACT1, FACT2, etc. mapa, mapai (integer), mapaf (float). Modos de motor MODO0, normal; 1, sin actualización (imprime el fondo y todos los sprites y después actualiza toda la pantalla); 2, 3D (con estructuras especiales de Ente). Define en cabecera para suprimir sonido #define SOUND. Módulos en ficheros separados: argumentos de escena tı́picos, GUI, texto, etc. Interpolación tres nodos (cuadrática). Variable global con el estado, incluyendo fullscreen, sonido desactivado, modo, cursor desactivado... Al entrar a escena se podrı́a consultar estas variables y proceder en consecuencia (Toggle o ShowCursor). Sepodrı́an hacer llamadas vacı́as escena con este propósito. Añadir imprim. Añadir fondoactual. Nuevas variables V4_RATONX, V4_RATONY, para las coordenadas de la posición del ratón. Creación automática de capas trasparentes. MiMotor1.1 12. 16 Más ejemplos Nombre: gravedad.c Descripción: Caı́da libre de una bolita con rebote en el suelo y pérdida de velocidad por el rozamiento. Se termina cuando se alcanza el borde derecho o la velocidad horizontal es menos que una décima. Ajustando los valores ACEL y ROZA ası́ como la velocidad inicial, en este ejmplo (5.0,0.0), se consiguen diferentes comportamientos. /* /ficheros/gravedad.c */ #include "MiMotor1.1.h" #define DIRECI "./images/" #define DIRECS "./sounds/" /* Directorio de imágenes */ /* Directorio de sonidos */ void c_rebota(Ente *ser) { #define ACEL 1.0 #define ROZA 0.75 ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenadas */ ser->vy+=ACEL; /* Suma aceleración */ ser->x+=ser->vx; ser->y+=ser->vy; /* A~ nade la velocidad */ if(ser->y>ALTO-ser->cara->h ){ /* Choque contra el suelo */ ser->y=ALTO-ser->cara->h; ser->vy*=-ROZA; ser->vx*=ROZA; /* Rozamiento en rebote */ } if( (ser->x>ANCHO-ser->cara->w) || (ser->vx<0.1) ) V2_FLAGSS|=FESC; } void creaunabolita(void){ cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ nace(0,0,0.0,0.0,5.0,0.0,c_rebota,album[0]); } int main(int argc, char *argv[]){ motoron(); escena(creaunabolita,NULL,NULL,NULL); motoroff(); return EXIT_SUCCESS; } Nombre: mueve.c Descripción: Permite mover una bolita con las teclas de los cursores. Pulsando la barra espaciadora se termina. /* /ficheros/mueve.c */ #include "MiMotor1.1.h" #define DIRECI "./images/" #define DIRECS "./sounds/" /* Directorio de imágenes */ /* Directorio de sonidos */ void c_rebro(Ente *ser) { ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenada */ if(V2_ESTTEC&TARR) ser->y-= 8.0; MiMotor1.1 17 if(V2_ESTTEC&TABA) ser->y+= 8.0; if(V2_ESTTEC&TIZQ) ser->x-= 8.0; if(V2_ESTTEC&TDCH) ser->x+= 8.0; if(ser->y<0){ ser->y=0; } if(ser->y>ALTO-ser->cara->h){ ser->y=ALTO-ser->cara->h; } if(ser->x<0){ ser->x=0; } if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; } if(V2_ESTTEC&TSAL) V2_FLAGSS|=FESC; /* Termina */ } void creaunabolita(void){ cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ nace(0,200,320.0,400.0,9.0,0.0,c_rebro,album[0]); } int main(int argc, char *argv[]){ motoron(); TECLAS[2]=SDLK_UP; TECLAS[3]=SDLK_DOWN; TECLAS[1]=SDLK_LEFT; TECLAS[0]=SDLK_RIGHT; TECLAS[4]=SDLK_SPACE; escena(creaunabolita,cincotec,NULL,NULL); motoroff(); return EXIT_SUCCESS; } Nombre: colmul.c Descripción: Bolitas que chocan rebotando. La animación se para pulsando la tecla de pausa. Para continuar se usa a continuación Intro y para salir Escape. /* /ficheros/colmul.c */ #include "MiMotor1.1.h" #define DIRECI "./images/" #define DIRECS "./sounds/" /* Directorio de imágenes */ /* Directorio de sonidos */ #define ROJO SDL_MapRGB(screen->format,223,47,47) /*************/ /* cerebro */ /************/ void c_rebota(Ente *ser) { ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenada */ /* Velocidad con signo */ ser->x+=(1-2*V2_ESTTEC)*ser->vx; ser->y+=(1-2*V2_ESTTEC)*ser->vy; /* Rebotes */ if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; ser->vx*=-1;} if(ser->x<0){ ser->x=0; ser->vx*=-1;} if(ser->y>ALTO-ser->cara->w){ ser->y=ALTO-ser->cara->h; ser->vy*=-1;} if(ser->y<0){ ser->y=0; ser->vy*=-1;} } /***************/ /* CREASERES */ /***************/ void creaunabolita(void){ int i=40; float th; MiMotor1.1 18 cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ while(i--){ th=M_PI*(rand() % 20)/10.0; nace(1, 0, rand() % (ANCHO-album[0]->w), rand() % (ALTO-album[0]->h), 5.0*cos(th), 5.0*sin(th),c_rebota,album[0]); } } /**************/ /* INTERRUP */ /**************/ /* Auxiliar */ char escapa(void){ /* Vuelve 1 si se ha pulsado enter o esc, cero en otro caso */ Uint8 *keystate = SDL_GetKeyState(NULL); SDL_Event event; /* Si no hay tecla accionada, vuelve con cero*/ if(SDL_PollEvent(&event)){ if((event.type!=SDL_KEYDOWN)&&(event.type!=SDL_KEYUP)) if(keystate[SDLK_ESCAPE]){ /* Sale */ V2_FLAGSS|=FESC; return 1; /* Escape */ } if(keystate[SDLK_RETURN]) return 1; /* Pausa */ } return 0; return 0; } Uint32 pausayescape(Ente *ser, Uint32 ahora){ if(V2_FLAGSS&FPAU){ while(!escapa()); V2_FLAGSS^=FPAU; /* Lo quita */ return SDL_GetTicks(); } if(V2_FLAGSS&FESC) return 0; return ahora; } /**************/ /* COLISION */ /**************/ void chocarebota(Ente *ser){ int i; int nser=ser-adan; /* Número de ser */ float vx,vy; /* Radiovector */ float t; for(i=1; i<V2_NSERAC-nser; ++i){ /* Estudia si ser y ser+i se han chocado */ vx=(ser+i)->x - ser->x; vy=(ser+i)->y - ser->y; t=vx*vx+vy*vy; /* Norma al cuadrado */ if( t<16*16 ){ if( (t=sqrt(t)) == 0 ) t=vx=1.0; /* Por si coinciden */ t/=5.0; /* Direcciones opuestas al radiovector */ ser->vx= -vx/t; ser->vy= -vy/t; (ser+i)->vx= vx/t; (ser+i)->vy= vy/t; return; /* Impide colisión múltiple */ } } } int main(int argc, char *argv[]){ motoron(); escena(creaunabolita,cincotec,pausayescape,chocarebota); motoroff(); MiMotor1.1 19 return EXIT_SUCCESS; } Nombre: araton.c Descripción: Al pinchar sobre la pantalla con el ratón, una bolita se acerca al cursor. Hay un efecto de deceleración según se acerca al destino. Se sale cerrando la pantalla. /* /ficheros/araton.c */ #include "MiMotor1.1.h" #define DIRECI "./images/" #define DIRECS "./sounds/" /* Directorio de imágenes */ /* Directorio de sonidos */ void c_araton(Ente *ser) { ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenada */ if( V2_ESTTEC ) { ser->x+= (V4_ALMACX0-ser->x)/7.0; ser->y+= (V4_ALMACY0-ser->y)/7.0; } if(ser->y<0){ ser->y=0; } if(ser->y>ALTO-ser->cara->h){ ser->y=ALTO-ser->cara->h; } if(ser->x<0){ ser->x=0; } if(ser->x>ANCHO-ser->cara->w){ ser->x=ANCHO-ser->cara->w; } } void creaunabolita(void){ cargaspri(DIRECI "bolita.png",0,NULL); /* Carga imagen */ nace(0,0,ANCHO/2,ALTO/2,0.0,0.0,c_araton,album[0]); } void leeraton(void){ SDL_Event event; if( !SDL_PollEvent(&event) ) return; if (event.type==SDL_QUIT) V2_FLAGSS|=FESC; /* Activa flag de escape */ if( event.type!=SDL_MOUSEBUTTONDOWN ){ V2_ESTTEC= 0; return;} V2_ESTTEC= 1; V4_ALMACX0= event.button.x; V4_ALMACY0= event.button.y; } int main(int argc, char *argv[]){ motoron(); escena(creaunabolita,leeraton,NULL,NULL); motoroff(); return EXIT_SUCCESS; } Nombre: adeborra.c Descripción: Ejemplo jugable. Hay que mover la burbuja grande con las teclas Q, A, O, P para recoger las gotatias que hay por la pantalla evitando los cristales de sal. La burbuja se moverá por inercia, para detenerla hay que pulsar la barra espaciadora. /* /ficheros/adeborra.c */ #include "MiMotor1.1.h" MiMotor1.1 20 #define DIRECI "./images/" #define DIRECS "./sounds/" #define #define #define #define /* Directorio de imágenes */ /* Directorio de sonidos */ AN 520 AL 320 NSAL 10 NGOT 20 /**************/ /* cerebros */ /*************/ void c_prota(Ente *ser){ ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenadas */ /* Nueva posición */ if( V2_ESTTEC&TDCH ) ser->x+=ser->vx; if( V2_ESTTEC&TIZQ ) ser->x-=ser->vx; if( V2_ESTTEC&TARR ) ser->y-=ser->vy; if( V2_ESTTEC&TABA ) ser->y+=ser->vy; /* Impide que se salga del recuadro */ if( ser->x<(ANCHO-AN)/2 ) ser->x=(ANCHO-AN)/2; if( ser->x>(ANCHO+AN)/2-ser->cara->w ) ser->x=(ANCHO+AN)/2-ser->cara->w; if( ser->y<(ALTO-AL)/2 ) ser->y=(ALTO-AL)/2; if( ser->y>(ALTO+AL)/2-ser->cara->w ) ser->y=(ALTO+AL)/2-ser->cara->w; /* Si está muerto, if( V2_FLAGSS&FRIP /* Si está sólo se if( V2_NSERAC==1 ) se rompe */ ) ser->cara= album[3]; acaba */ V2_FLAGSS|=FESC; } void c_rebota(Ente *ser){ ser->xa=ser->x; ser->ya=ser->y; /* Actualiza coordenadas */ ser->x+=ser->vx; ser->y+=ser->vy; /* A~ nade la velocidad */ /* Rebotes */ if(ser->x>(ANCHO+AN)/2-ser->cara->w){ ser->x=(ANCHO+AN)/2-ser->cara->w; ser->vx*=-1;} if(ser->x<(ANCHO-AN)/2){ ser->x=(ANCHO-AN)/2; ser->vx*=-1;} if(ser->y>(ALTO+AL)/2-ser->cara->w){ ser->y=(ALTO+AL)/2-ser->cara->h; ser->vy*=-1;} if(ser->y<(ALTO-AL)/2){ ser->y=(ALTO-AL)/2; ser->vy*=-1;} /* Si ya no hay gotas mueren todas las sales */ if( V2_NSERAC <= NSAL+1 ) ser->tipo|=MUER; } /***************/ /* CREASERES */ /***************/ void creagotas(void){ int i=NSAL; /*Número de gotitas */ float th; cargaspri(DIRECI "bola.png",0,NULL); /* imagen protra */ cargaspri(DIRECI "sal.png",0,NULL); /* imagen sales*/ cargaspri(DIRECI "gota.png",0,NULL); /* imagen gotitas */ cargaspri(DIRECI "bolarota.png",0,NULL); /* imagen protra roto */ /* nace(tipo,cont,(x,y),(vx,vy),cerebro,cara) */ /* protra */ nace(0,200,(ANCHO-AN)/2,(ALTO-AL)/2,6.2,6.2,c_prota,album[0]); /* sales y gotitas*/ while(i--){ th=M_PI*(rand() % 100)/50.0; nace(1,0,ANCHO/2,ALTO/2,5.1*cos(th),5.1*sin(th),c_rebota,album[1]); } i=NGOT; while(i--){ th=M_PI*(rand() % 100)/50.0; nace(2,0,(ANCHO-(AN-31))/2+(rand() % (AN-31)), (ALTO-(AL-31))/2+(rand() % (AL-31)), MiMotor1.1 21 0.03*cos(th),0.03*sin(th),c_rebota,album[2]); } } /************/ /* LEETEC */ /************/ void lee5tec(void){ Uint8 *keystate = SDL_GetKeyState(NULL); SDL_Event event; /* Si hay eventos */ if(SDL_PollEvent(&event)){ /* Termina si se cierra la ventana */ if (event.type==SDL_QUIT) V2_FLAGSS|=FESC; /* Activa flag de escape */ /* Vuelve si no hay teclas pulsadas (sigue como estaba) */ if((event.type!=SDL_KEYDOWN)&&(event.type!=SDL_KEYUP)) return; if(keystate[TECLAS[0]]){ if(keystate[TECLAS[1]]){ if(keystate[TECLAS[2]]){ if(keystate[TECLAS[3]]){ if(keystate[TECLAS[4]]){ V2_ESTTEC&= ~TIZQ; V2_ESTTEC|= V2_ESTTEC&= ~TDCH; V2_ESTTEC|= V2_ESTTEC&= ~TABA; V2_ESTTEC|= V2_ESTTEC&= ~TARR; V2_ESTTEC|= V2_ESTTEC= 0;} /* Parado */ TDCH;} TIZQ;} TARR;} TABA;} } } /**************/ /* INTERRUP */ /**************/ Uint32 escymarcador(Ente *ser,Uint32 ahora){ int i=V4_PUNTUA; /* Punto base de impresión del marcador */ SDL_Rect r={153,438,0,0}; if(V2_FLAGSS&FACT){ V2_FLAGSS^=FACT; /* Lo quita */ /* Posición gota */ while(i--){ r.x+= 19; SDL_BlitSurface(album[2],NULL,screen,&r); /* Copia a pantalla */ SDL_UpdateRect(screen,r.x,r.y,r.w,r.h); /* Actualiza */ } } if( (V2_FLAGSS&FESC) || (V2_FLAGSS&FRIP) ){ SDL_Delay(2000); /* Espera 2s */ return 0; /* Sale de escena */ } return ahora; } /**************/ /* COLISION */ /**************/ void coli(Ente *ser){ int i,j,dc; if( ser->tipo ) return; /* Si no es el prot., vuelve */ for(i=1;i<V2_NSERAC;++i){ /* Distancia al cuadrado centro prot - centro otro ser */ dc = ser->x + ser->cara->w/2 - (ser+i)->x - (ser+i)->cara->w/2; j = ser->y + ser->cara->h/2 - (ser+i)->y - (ser+i)->cara->h/2; dc*= dc; dc+= j*j; if( (ser+i)->tipo == 1 && (dc<324) ){ /* Enemigo */ V2_FLAGSS|= FRIP; /* Activa interrupción */ MiMotor1.1 22 } if( ((ser+i)->tipo == 2) && (dc<121) ){ /* Gotita */ /* 121 aprox (radio prot - radio gotita)^2 */ (ser+i)->tipo|= MUER; ++V4_PUNTUA; /* Incrementa puntos */ V2_FLAGSS|= FACT; /* Activa interrupción */ } } } void imprim(char *fich,Uint32 x, Uint32 y){ SDL_Rect r={x,y,0,0}; SDL_Surface *imag=cargimag(fich); SDL_BlitSurface(imag,NULL,screen,&r); SDL_FreeSurface(imag); } int main(int argc, char *argv[]){ motoron(); /* Decoración */ imprim(DIRECI "decor.jpg",0,0); SDL_Flip(screen); /* Teclas */ TECLAS[0]=SDLK_p; /* Derecha */ TECLAS[1]=SDLK_o; /* Izquierda */ TECLAS[2]=SDLK_a; /* Arriba */ TECLAS[3]=SDLK_z; /* Abajo */ TECLAS[4]=SDLK_SPACE; /* Abajo */ /* Puntos a cero */ V4_PUNTUA=0; /* Color de fondo */ V4_CFONDO=SDL_MapRGB(screen->format,4,0,77); escena(creagotas,lee5tec,escymarcador,coli); motoroff(); return EXIT_SUCCESS; } 13. Apéndice: Instalar y compilar con SDL Compilación: En Linux se cuenta con el bien conocido compilador gcc y los proyectos en C de un solo fichero se compilan en la lı́nea de comandos con gcc fuente.c -o ejecutable.out -lm (lo último es necesario si se usa <math.h> ). Para compilar empleando las bibliotecas SDL (incluyendo las de imágenes, sonido y texto) se puede usar gcc fuente.c -o ejecutable.out ` sdl-config --cflags --libs` -lSDL image -lSDL mixer -lSDL ttf -lm (es importante que las comillas sean las izquierdas), en particular para usar fuente.c con el motor gcc fuente.c MiMotor1.1.c -o ejecutable.out ‘sdl-config --cflags --libs‘ -lSDL image -lSDL mixer -lm. Si se quieren añadir las bibliotecas de OpenGL hay que incluir también -lGL -lGLU. En Windows hay varios IDE’s y compiladores. Más abajo se indica cómo emplear las bibliotecas SDL con los IDE gratuitos y libres Kdevelop (Linux) y DevCpp (Windows). MiMotor1.1 23 Ejecutables windows y .dll para SDL: Los ejecutables windows que utilicen la biblioteca SDL necesitan incluir las .dll correspondientes en el directorio en el que esté el ejecutable. Varios ejecutables pueden compartir las mismas .dll (de hecho si las añadimos en la carpeta del sistema todos lo harán). Las .dll asociadas a SDL y sus bibliotecas asociadas más comunes son SDL.dll, SDL image.dll, jpeg.dll, libpng12.dll, zlib1.dll, SDL mixer.dll, SDL ttf.dll, libfreetype-6.dll. La primera corresponde a la biblioteca básica, las cuatro siguientes a SDL image, SDL mixer.dll sólo se emplea si hay sonido y las dos últimas corresponden a la biblioteca TTF. Editores: kwrite y kate son dos buenos editores para escribir código bajo el entorno KDE. El último permite dividir la ventana en dos o más marcos de forma que la copia y revisión de código es más sencilla. Para proyectos largos una buena opción es KDevelop. Serı́a conveniente al emplear SDL activar el completado de código en KDevelop pero la documentación no se ha mostrado clara en este punto. Parece que con plug-ins adecuados es posible hacerlo también en kwrite y kate. Es un poco molesto cuando se aprovecha código anterior que el indentado por tabulación en KDevelop no coincida con el de kwrite y kate. Emplear la biblioteca SDL con Kdevelop: Lo siguiente quizá no sea la forma más ortodoxa pero funciona, al menos en la máquina en uso (bajo SuSE Linux 10.0). Seguir los siguientes pasos: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. Proyecto>nuevo. C>programa hola mundo. Nombre: nomfich, Localización: mipath. Opciones (adelante). Sistema de control de versiones ninguno (adelante). Plantilla cabeceras (cambiar lo que se quiera, por ejemplo YEAR por DATE) (adelante). Plantilla de .c, lo mismo (adelante). Finalizar. Cambiar el código por nuestro programa SDL. Opciones del proyecto>C>parámetros del compilador CFLAGS: -O0 -g3 y lo que salga de ‘sdl-config --cflags --libs‘ y las librerı́as que se quieran usar, en la máquina en uso -I/usr/include/SDL -D REENTRANT -L/usr/lib64 -lSDL -lpthread -lSDL image -lSDL mixer -lSDL ttf -lm. Construir proyecto y ejecutar. Emplear la biblioteca SDL con DevCpp: Los ficheros se toman del sitio SDL http: //www.libsdl.org/ y puede haber versiones más modernas. Biblioteca SDL 1. Descomprimir SDL-devel-1.2.13-mingw32 2. Copiar \SDL-devel-1.2.13-mingw32\SDL-1.2.13\bin\SDL.dll en el directorio del sistema C: \WINDOWS\system 3. 4. 5. 6. Copiar el contenido de \SDL-devel-1.2.13-mingw32\SDL-1.2.13\lib en C:\Dev-Cpp\lib Copiar en C:\Dev-Cpp\include la carpeta \SDL-devel-1.2.13-mingw32\SDL-1.2.13\include Se abre Devcpp y se crea console application>sdl1 (elegir C como lenguaje) Herramientas → Opciones del Compilador MiMotor1.1 24 7. |x| Añadir estos comandos a la lı́nea de comandos del linker -lmingw32 -lSDLmain -lSDL -mwindows 8. Probar con sdl1.c incluido más abajo. Bibliotecas SDL image y SDL mixer 1. Descomprimir SDL_image-devel-1.2.6-VC8 y SDL_mixer-devel-1.2.8-VC8 2. El contenido de SDL_image-devel-1.2.6-VC8\SDL_image-1.2.6\include se copia a a C:\Dev-Cpp\ include\SDL (el fichero SDL image.h) y lo mismo con el contenido de \SDL_mixer-devel-1.2. 8-VC8\SDL_mixer-1.2.8\include (el fichero SDL mixer.h) 3. El contenido de SDL_image-devel-1.2.6-VC8\SDL_image-1.2.6\lib se copia a C:\Dev-Cpp\lib y lo mismo con el de SDL_mixer-devel-1.2.8-VC8\SDL_mixer-1.2.8\lib 4. Copiar a C:\WINDOWS\system las dll de \SDL_image-devel-1.2.6-VC8\SDL_image-1.2.6\lib y las dll de SDL_mixer-devel-1.2.8\lib 5. Herramientas → Opciones del Compilador 6. |x| Añadir estos comandos a la lı́nea de comandos del linker -lmingw32 -lSDLmain -lSDL -lSDL image -lSDL mixer -mwindows 7. Probar con sdl2.c incluido más abajo usando blip.wav y bolita.png Fichero de prueba sdl1.c /* /ficheros/SDL/sdl1.c */ #include <stdio.h> #include <SDL/SDL.h> void imprimir_rectangulo (SDL_Surface *screen, int x, int y); int main(int argc, char *argv[]) { SDL_Surface *screen; SDL_Event event; if (SDL_Init(SDL_INIT_VIDEO) == -1) { printf("Error: %s\n", SDL_GetError()); return 1; } screen = SDL_SetVideoMode(320, 240, 16, SDL_SWSURFACE); if (screen == NULL) { printf("Error: %s\n", SDL_GetError()); return 1; } SDL_WM_SetCaption("Ejemplo básico", NULL); imprimir_rectangulo(screen, 20, 30); while (SDL_WaitEvent(&event)) { if (event.type == SDL_QUIT) break; } SDL_Quit(); return 0; } void imprimir_rectangulo (SDL_Surface *screen, int x, int y) { SDL_Rect rect = {x, y, 50, 50}; Uint32 a = SDL_MapRGB(screen->format, 255, 200, 100); MiMotor1.1 SDL_FillRect(screen, &rect, a); SDL_UpdateRect(screen, rect.x, rect.y, rect.w, rect.h); } Fichero de prueba sdl2.c /* /ficheros/SDL/sdl2.c */ #include <stdio.h> #include <stdlib.h> #include <SDL/SDL.h> #include <SDL/SDL_image.h> #include <SDL/SDL_mixer.h> #define ANCHO 320 /* Ancho de pantalla */ #define ALTO 240 /* Alto de pantalla */ SDL_Surface *screen; void suena(void){ Mix_Chunk *sonido; sonido = Mix_LoadWAV("blip.wav"); if(sonido==NULL) printf("error al cargar sonido\n"); Mix_PlayChannel (-1, sonido, 0); SDL_Delay(500); Mix_FreeChunk(sonido); Mix_CloseAudio(); } /*.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.*/ /*| Nombre: cargaimag |*/ /*. Descripción: Carga una imagen .*/ /*| Entra: Nombre de un fichero |*/ /*. Sale: Imagen (superficie SDL) .*/ /*-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-*/ SDL_Surface *cargimag(char *fichero){ SDL_Surface *temp,*ima; char *ficht; ficht=fichero; while(*ficht) ++ficht; /*Busca el final*/ if(*(--ficht)==’p’) /* Si la última letra es una "p" es bmp*/ temp=SDL_LoadBMP(fichero); else temp=IMG_Load(fichero); if (temp == NULL){ printf("Error al cargar %s\n", fichero); exit(EXIT_FAILURE); } if(*(--ficht)==’n’) /* Si la penúltima letra es una "n" es png */ ima=SDL_DisplayFormatAlpha(temp); else ima=SDL_DisplayFormat(temp); SDL_FreeSurface(temp); return ima; } void dibujabolas(void){ SDL_Surface *bolita; SDL_Rect r; int i; srand( time( NULL )); 25 MiMotor1.1 // for(i=0;i<1000;++i){ r.x=(rand()%(ANCHO-bolita->w)); r.y=(rand()%(ALTO-bolita->h)); bolita=cargimag("bolita.png"); SDL_BlitSurface(bolita,NULL,screen,&r); SDL_Flip(screen); SDL_Delay(10); } } int main(int argc, char *argv[]) { SDL_Event event; if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) == -1) { printf("Error: %s\n", SDL_GetError()); return 1; } screen = SDL_SetVideoMode(ANCHO, ALTO, 32, SDL_SWSURFACE); if (screen == NULL) { printf("Error: %s\n", SDL_GetError()); return 1; } // Mix_OpenAudio (44100, AUDIO_S16, 2, 4096); // Mix_OpenAudio(11025, AUDIO_U8, 1, 1024); // Mix_OpenAudio(22050, AUDIO_S16, 1, 2048); if(Mix_OpenAudio (44100, MIX_DEFAULT_FORMAT, 2, 1024)) { printf("No se puede abrir el audio %s\n", Mix_GetError()); exit(1); } atexit(Mix_CloseAudio); SDL_WM_SetCaption("Ejemplo básico 2", NULL); dibujabolas(); suena(); while (SDL_WaitEvent(&event)) { if (event.type == SDL_QUIT) break; } // // } SDL_Quit(); return 0; system("PAUSE"); return 0; 26