Realizado por
Dirigido por
Departamento
UNIVERSIDAD DE MÁLAGA
Presidente Dº/Dª.
Secretario Dº/Dª.
Vocal Dº/Dª.
Y PARA QUE CONSTE, SE EXTIENDE FIRMADA POR LOS COMPARECIENTES DEL TRIBUNAL, LA
PRESENTE DILIGENCIA.
I
3.2.6.2 Decimales con punto fijo.....................................................................................37
3.2.6.3 Trayectorias y ángulos........................................................................................38
3.2.7 Sonido..................................................................................................................42
3.2.7.1 Archivos de sonido Raw......................................................................................43
3.2.7.2 Archivos de sonido mod......................................................................................45
3.2.7.3 Funciones adicionales de sonido .........................................................................46
3.2.8 Programación Hardware DS ..............................................................................46
3.2.8.1 Fecha y hora ........................................................................................................46
3.2.8.2 Información de usuario.......................................................................................47
3.2.8.3 Pausa al cerrar ....................................................................................................48
3.2.8.4 Iluminación de pantallas.....................................................................................49
3.3 Programación 3D ................................................................................. 49
3.3.1 Inicializar funciones 3D ......................................................................................50
3.3.2 Polígonos .............................................................................................................52
3.3.3 Rotación ..............................................................................................................54
3.3.4 Figuras Básicas 3D..............................................................................................56
3.3.5 Texturas ..............................................................................................................59
3.3.6 Iluminación .........................................................................................................64
3.3.7 Seleccionar objetos..............................................................................................67
II
5.3 Estudio de las clases ............................................................................. 84
5.3.1 Diagrama de clases..............................................................................................84
5.3.2 Definición de las clases........................................................................................85
Capítulo 6. Implementación............................................................91
6.1 Recursos gráficos.................................................................................. 91
6.1.1 Sprites..................................................................................................................92
6.1.2 Creación objetos 3D............................................................................................92
6.1.2.1 Formato DirectX .................................................................................................96
6.1.2.2 Conversión de formato ..................................................................................... 105
6.2 Arquitectura de la aplicación ............................................................ 111
6.2.1 Librerías............................................................................................................ 111
6.2.1.1 Vectores.h.......................................................................................................... 111
6.2.1.2 glQuaternion.h .................................................................................................. 114
6.2.1.3 Librería lib ........................................................................................................ 116
6.2.2 Clase Figura ...................................................................................................... 120
6.2.3 Clase Objeto3D ................................................................................................. 121
6.2.4 Clase Entidad .................................................................................................... 123
6.2.5 Clase Personaje ................................................................................................. 125
6.2.6 Clase Escenario ................................................................................................. 126
6.2.6.1 Constructor Escenario ...................................................................................... 136
6.2.6.2 Ciclo de juego .................................................................................................... 141
6.2.6.3 Búsqueda de camino ......................................................................................... 159
6.2.7 Crear nuestra propia aventura. Especializaciones de escenario y entidad..... 163
6.2.7.1 Herencia de Entidad ......................................................................................... 164
6.2.7.2 Herencia de Escenario ...................................................................................... 165
6.2.8 Fichero main ..................................................................................................... 173
6.3 Implementar nuestra aventura.......................................................... 174
6.3.1 Protagonista ...................................................................................................... 175
6.3.2 Entidades........................................................................................................... 175
6.3.2.1 Mesa .................................................................................................................. 175
6.3.2.2 Estatua............................................................................................................... 176
6.3.2.3 Puerta ................................................................................................................ 177
III
6.3.2.4 Fregona.............................................................................................................. 177
6.3.2.5 Llave .................................................................................................................. 178
6.3.2.6 Árbol.................................................................................................................. 179
6.3.2.7 Coco................................................................................................................... 179
6.3.3 Escenarios ......................................................................................................... 180
6.3.3.1 Escenario Habitación........................................................................................ 180
6.3.3.2 Escenario Exterior ............................................................................................ 185
6.3.4 Fichero main ..................................................................................................... 188
Apéndice A. Glosario.....................................................................197
Bibliografía.....................................................................................205
IV
DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Introducción
Capítulo 1. Introducción
1.1 Antecedentes
Los videojuegos han acompañado a la informática prácticamente desde su nacimiento
convirtiéndose en una muestra de los avances hardware y software que se iban realizando en
el sector. Se convirtieron en una de las maneras de entretenerse para la mayoría de aquellos
que estaban relacionados con la informática, quedando apartados de la gente ajena a este
entorno. Una vez que se empieza a ver la popularidad que alcanzan los videojuegos,
comienzan a formarse grandes empresas que darán lugar a la llamada industria de los
videojuegos. Estas empresas desarrollarán juegos para ordenadores personales,
microordenadores (Commodore 64 o Spectrum) y sistemas domésticos o videoconsolas. Hoy
día es una industria consolidada que mueve mucho dinero y en continuo aumento, que ha
demostrado la capacidad que tienen los videojuegos para entretener y enseñar.
Actualmente las empresas desarrollan sus propias herramientas de desarrollo y además, las
que poseen alguna videoconsola en el mercado disponen de sus propios kit’s de desarrollo con
el hardware y software necesario para probar las aplicaciones. Sin embargo, existen
alternativas a los SDK (Software Development Kit) propietarios de las compañías, que surgen
gracias al aporte de personas que programan y distribuyen librerías con licencias abiertas que
permiten desarrollar para videoconsolas. Estos videojuegos creados de manera no profesional
son conocidos como homebrew (aplicaciones informáticas realizadas por aficionados para
plataformas de videojuegos propietarias). En el caso concreto de la videoconsola portátil
Nintendo DS (NDS) de Nintendo, existen dos librerías popularizadas llamadas libnds [4] y
PAlib [5] que están incluidas en Devkitpro [6], que es un software que incluye todas las
herramientas necesarias para el desarrollo de varias consolas, incluída Nintendo DS [7].
Libnds es la librería de desarrollo estándar para NDS que permite programar en C/C++ a un
nivel de abstracción algo bajo ya que requiere conocer los registros de la consola. Por tal
motivo existe una segunda opción, la librería PAlib, que está implementada sobre la anterior y
que facilita muchas tareas, por lo que se recomienda para comenzar. Con estas herramientas
se pueden crear archivos ejecutables para la consola. Además existen emuladores que
funcionan en PC, que permiten probar estos ejecutables sin tener que cargarlos en la consola.
Ejemplos de estos emuladores son: DSemu [8], Daulis [9], Ideas [10].
1.2 Objetivos
El objetivo de este proyecto será desarrollar un videojuego de tipo aventura en 3D para la
videoconsola Nintendo DS haciendo uso de las librerías libnds y PAlib. El lenguaje utilizado
será C++ y para la programación gráfica 3D se hará uso de una versión limitada de OpenGL
implementada en la videoconsola.
La videoaventura estará compuesta por dos niveles por los cuales habrá que manejar al
protagonista para que recoja e interactúe con objetos de los escenarios para poder ir
avanzando. Para manejar al personaje, el jugador usará los botones de la consola y un puntero
para la pantalla táctil. Cada nivel tendrá un escenario fijo del que será visible sólo una parte,
según la posición del personaje. Será una imagen que mostrará la proyección plana del
escenario tridimensional. El personaje del juego y los objetos con los que interactúan serán
modelados tridimensionalmente y se moverán dentro del escenario. El estilo visual de la
videoaventura estará inspirado en aventuras clásicas como Alone in de Dark [11] o Grim
Fandago [12].
Se hará uso de una metodología que abarque los diferentes campos para el desarrollo del
videojuego. Elaboración de un guión que refleje los diferentes estados en los que estarán la
aplicación y el personaje principal. Diseño de la estructura lógica de los niveles y definición
de los mismos en base al guión. Diseño y creación de personajes, objetos y escenarios.
Modelado y texturizado de los elementos en 3D y animación del personaje. Obtener música y
efectos sonoros. Estructura de la aplicación orientada a objetos. Implementación de técnicas
de colisión de objetos.
definición de la idea, el guión de la aventura, los conceptos que formarán parte del juego y
finalmente los casos de uso de los elementos que lo componen.
• Capítulo 6. Implementación:
Se trata del capítulo con mayor contenido de la memoria, que explica todo el desarrollo de la
aplicación basándose en los dos capítulos anteriores de análisis y diseño. Primero se
implementan los recursos gráficos como imágenes 2D y 3D, después el núcleo de la
aplicación y por último de qué manera se puede utilizar esta estructura para crear la aventura
que hemos definido.
• Capítulo 7. Conclusiones:
Por último, un capítulo de conclusiones donde se expresan las impresiones recopiladas a lo
largo de la elaboración del proyecto. Esto incluye inconvenientes que han podido ir surgiendo
o simplemente mejoras u opiniones de la aplicación, y un segundo apartado dedicado a
posibles desarrollos que puedan mejorarla.
Los controles habituales como el D-pad (mando de direcciones), los botones de acción,
Power, Start y Select; están situados en la parte inferior de la consola a ambos lados de la
pantalla. Tiene además dos botones colocados en las esquinas superiores de la parte inferior.
En la zona superior se encuentra la segunda pantalla, la cual no es sensible al tacto, y los
altavoces estéreo. En la parte izquierda de la pantalla inferior se encuentra un micrófono.
2.1 Hardware
Algunas características hardware de la videoconsola NDS que pueden resultar necesarias
conocer para desarrollar aplicaciones son:
• La resolución de las pantallas LCD es de 256 x 192 píxeles cada una.
• Dispone de dos procesadores ARM: un ARM9 principal, y un co-procesador ARM7; de
67MHz y 33 MHz, respectivamente, con 4 MB de memoria principal. El ARM7 se
encarga de manejar la salida de audio y la pantalla táctil mientras que el procesador
principal maneja los gráficos y el resto de procesamiento incluyendo el cómputo de 3D.
• El sistema hardware 3D permite transformaciones y luces, transformaciones de
coordenadas de textura, mapeado de texturas, transparencias alfa, anti-aliasing, cel
shading y z-buffering. El sistema es capaz teóricamente de manejar 120000 triángulos por
segundo, a 60 frames por segundo (FPS). Además del límite de 2048 triángulos por frame
a 60 FPS. El sistema está diseñado para renderizar sobre una pantalla la escena 3D,
aunque existen algunas posibilidades hacerlo sobre ambas pantallas, esto provocaría una
pérdida de rendimiento significativa.
• Permite comunicaciones inalámbricas Wi-Fi con un punto de acceso estándar, o bien con
otra consola Nintendo DS. Al conectarse a Internet, se puede acceder a la red Nintendo
Wi-Fi para poder competir con otros usuarios.
• Existen dos ranuras en la parte inferior, para introducir los juegos: la más pequeña está
situada arriba y se utiliza para poder jugar a los videojuegos de la propia consola Nintendo
DS, la más grande que se encuentra abajo sirve para introducir juegos de la videoconsola
portátil antecesora de Nintendo, Game Boy Advance, permitiendo retrocompatibilidad y
seguir disfrutando de juegos antiguos en la nueva videoconsola.
posible. Se necesitará disponer de un hardware que emule a los cartuchos oficiales, con una
memoria de almacenamiento que podamos gestionar para poder cargar o borrar nuestras
aplicaciones. La técnica empleada ha ido evolucionando y perfeccionándose con el tiempo. Al
principio se hacía uso de la ranura de juegos para Game Boy Advance para cargar las
aplicaciones homebrew por el simple motivo de que ya estaba implantado un sistema de carga
de aplicaciones de este tipo para dicha consola predecesora, por tanto se aprovechaba el
sistema de retrocompatibilidad de la consola reutilizando el hardware ya desarrollado. Por
tanto, con este sistema, es necesario un cartucho similar al de Game Boy Advance que
disponga de un sistema de memoria, bien podía estar incluída en el propio cartucho o podía
tener un ranura para introducir una tarjeta de memoria Flash SD (figura 2):
Sin embargo, al ser esta una tecnología reutilizada, no funcionaba por sí sola ya que por la
ranura de juegos de Game Boy Advance sólo se pueden ejecutar juegos de Game Boy
Advance, por tanto si la aplicación homebrew era para Nintendo DS hacía falta alguna manera
de hacérselo saber a la consola. Para lograr esto existen varias técnicas, por ejemplo una de
las primeras fue flashear, lo que significaba cambiar el firmware de esta con el riesgo que esto
suponía si había algún problema durante este proceso. Para evitar tener que modificar el
software interno de la consola se crearon las herramientas de inicio (booting tools) que se
situaban en la ranura de juegos para Nintendo DS, haciendo que la cuando la consola vaya a
cargar la aplicación y pretenda hacerlo en la ranura de Nintendo DS, la herramienta de inicio
indique que la información a cargar está en la ranura de Game Boy Advance. Por tanto, con
esta solución hardware no es necesario modificar el firmware, haciendo falta sin embargo un
segundo cartucho similar a los de Nintendo DS (figura 3):
Otra solución aparecida con posterioridad, consiste en utilizar tan sólo un dispositivo
hardware para la carga de las aplicaciones. Se trata de un cartucho similar al de los juegos de
Nintendo DS que dispone de un sistema de almacenamiento, bien interno o con la posibilidad
de insertarle una tarjeta flash. De este modo no hace falta ningún sistema adicional para que la
consola cargue las aplicaciones, ya que estás se encuentran en la ranura correspondiente en
lugar de la ranura disponible para los juegos de Game Boy Advance como ocurría antes. Al
hacer falta tan sólo un único dispositivo, este es el sistema que actualmente más se utiliza para
la carga de homebrew (figura 4):
Lo primero que hay que tener en cuenta es la gran velocidad de evolución que presenta el
desarrollo expuesto a la comunidad de Internet. Es algo que se puede apreciar en el punto
anterior enfocado al hardware donde en muy poco tiempo se ha mejorado mucho la tecnología
coexistiendo en el mercado los productos más recientes junto con los primeros que
aparecieron, debido a que estos últimos no han sido absorbidos todavía por la demanda. Lo
mismo ocurre con las soluciones software ofrecidas para el desarrollo, existen varias librerías,
entornos de desarrollo, distintos lenguajes de programación disponibles y gran cantidad de
homebrew que da la impresión de mal organizado y caótico. Todo esto antes no ha existido ya
que se apoya en el uso de Internet donde grupos de usuarios se aglutinan en comunidades
temáticas y pueden fácilmente compartir sus librerías o aplicaciones para que otros las usen,
mejoren o simplemente les den más publicidad. De tal manera que se debe ver como una
ventaja y no como algo agobiante ya que por cada herramienta software disponible, está
detrás una comunidad de usuarios con foros de ayuda donde podrán resolver las dudas si es
que no están resueltas ya.
2.2.1 Libnds
Libnds [4], o también conocida en su versión anterior como ndslib, fue creada y mantenida
por Michael Noland (de apodo Joat), y Jason Rogers (Dovoto), además de haber contribuido
mucha gente en su mejora. Esta librería comenzó a ser la primera alternativa al SDK oficial de
Nintendo para DS. Permite crear aplicaciones que pueden ser cargadas por los elementos
hardware propios de la videoconsola Nintendo DS, explicados anteriormente. Con esta
librería es posible programar todas las características de la videoconsola como son: pantalla
táctil, micrófono, hardware 3D, hardware 2D y protocolo wifi. Esta librería contiene
información sobre los registros, abstrayendo la información de las direcciones de memoria.
Esto supone las mismas ventajas e inconvenientes que cualquier otra herramienta de
desarrollo que tienen bajo nivel de abstracción. El programador tiene mucha responsabilidad
al trabajar con los registros lo que supone tener un gran conocimiento de la plataforma,
pudiendo ser esto una ventaja porque otorga mucha flexibilidad y poder exprimir al máximo
la funcionalidad de la videoconsola.
La librería libnds se divide en varios archivos que abarcan las diferentes funciones y registros
para interactuar con la consola. La parte que maneja la visualización 2D tiene por ejemplo
funciones específicas para trabajar con la VRAM (memoria de video) donde se situarán las
paletas, texturas, sprites… también incluye una apartado 3D que ofrece una versión reducida
de la famosa librería openGL. Para la programación gráfica es necesario indicar el modo de
video antes de poder representar nada, cada modo tiene características diferentes como por
ejemplo el número y tipo de fondos, permitir rotaciones de los gráficos, otro servirá para
representación en 3D, etc.
Con esta librería se generan dos códigos principales: arm9.cpp y arm7.cpp. Cada uno de
los cuales es manejado por el procesador correspondiente, realizando las tareas específicas de
cada uno teniendo en cuenta los usos anteriormente descritos.
Otra parte importante para la programación haciendo uso de esta librería son las
interrupciones. Están vinculadas a los eventos del teclado para la comunicación con el
usuario, o con el pintado de buffer sobre la pantalla para manejar temporizaciones o para
controlar los frames por segundo de la aplicación, por ejemplo. Cuando sucede la interrupción
se guarda en un registro de peticiones de interrupción siendo tarea del programador atenderlas
o no.
Con esta aproximación se muestra el nivel de abstracción que usa esta librería y dependerá de
cada proyecto si puede venir mejor o peor usarla. De este modo la siguiente librería que se va
a explicar se convierte en la alternativa perfecta ya que está basada en libnds, pero presenta
multitud de funciones de alto nivel para simplificar la programación sin tener que estar
trabajando con registros, ni memorias de vídeo ni interrupciones.
2.2.2 PAlib
PAlib [5], se trata de una librería open source basada en libnds. Está compuesta por muchas
funciones que facilitan en gran medida el desarrollo de aplicaciones para la videoconsola
Nintendo DS. Al contrario que ocurría con la librería libnds que hacía falta conocer los
registros que tiene, con esta librería no se trata con el hardware directamente ya que dispone
de funciones de más alto nivel de abstracción que realizan funciones más complejas que a su
vez se comunican con la librería libnds. Esto ha hecho que mucha gente que comienza a hacer
aplicaciones para esta videoconsola opte por la librería PAlib. El apodo del creador de esta
librería es Mollusk y mantiene en la página web [5] gran cantidad de información como por
ejemplo: tutoriales en varios idiomas que abarcan la gran mayoría de las cosas que se pueden
hacer con la librería mediante ejemplos sencillos, documentación de las funciones
implementadas en la librería, un foro donde participan desarrolladores de todas partes de la
red y que pueden ayudar a resolver cualquier duda, índice con las aplicaciones que va creando
la comunidad e incluso para algunas de las cuales facilitan también su código, recursos
multimedia de distribución libre que otros desarrolladores pueden usar para sus proyectos...
Actualmente el creador mantiene una API [13] que divide a la librería en treinta módulos
diferenciados que van ampliándose según se incorporan nuevas funcionalidades. Por ejemplo,
hace poco la videoconsola ha adquirido la capacidad detectar movimientos o vibraciones
gracias a un cartucho que posee un sensor de movimiento y ya ha sido incluido en la librería
el módulo “DS Motion Commands” explicando las funciones que hacen falta para poder
manejar esta característica. La categoría de tutoriales es muy recomendable para ir
conociendo toda la funcionalidad que ofrece esta librería, aunque no está totalmente completa
pese a que trae mucha información. Esta organizada de momento en más de quince capítulos
que comienzan con la instalación de las librerías y un repaso a C, cubren la parte de
programación gráfica, sonido, programación de eventos, sistema de archivos, … y también
tratan conceptos más complejos como la programación en redes inalámbricas. En el siguiente
capítulo se estudiará esta librería para después plantear el análisis del juego final y finalmente
llevarlo a su desarrollo.
Para poder usar aplicaciones incluidas con PAlib como son PAGfx (conversor de gráficos),
PAFS (sistema de ficheros) o VHam (Ide de desarrollo); es necesario tener instalado .Net
Framework [14].
Hecho esto ya se puede instalar la librería PAlib a través de su instalador que se puede
descargar en su página web [5]. Su creador recomienda instalarla en la misma carpeta que
devkitpro para que todo funcione correctamente. También indica que se instalen devkitpro y
PAlib en rutas que no contengan espacios. Finalmente reiniciar el ordenador.
sirva de mucho ya que es algo en continua evolución como se ha comentado a lo largo de esta
documentación y en cualquier momento pueden aparecer otros emuladores que funcionen
perfectamente.
Ya se ha explicado anteriormente los distintos métodos y hardware que son necesarios para
cargar aplicaciones homebrew en la NDS, ahora basta con mencionar cual de los archivos
generados durante la compilación es el que hay que descargar, ya sea en la memoria interna o
en la tarjeta flash. Hablamos antes de tres ficheros creados al ejecutar el archivo por lotes
build.bat: HelloWorld.nds, HelloWorld.sc.nds y HelloWorld.ds.gba.
Todos son la misma aplicación compilada y sólo hará falta uno de ellos. El archivo con la
extención .ds.gba será el necesario si nuestro método para cargar aplicaciones es
introduciendo un cartucho de almacenamiento en el slot de Game Boy Advance (recordar que
si esto fuera así haría falta además un sistema para insertar en el slot de DS para que la
consola cargue las aplicaciones de DS en un slot distinto al que le corresponde). Sin embargo,
si se dispone de un sistema de carga de aplicaciones más actual, es decir de los que sólo hace
falta un dispositivo de almacenamiento en la ranura para DS, entonces el archivo que hace
falta es el que tiene la extensión .nds. Para métodos de la marca supercard, G6 o M3 se usa
el fichero con extención .sc.nds.
Otra parte importante es la edición del código. Existen distintos editores con los que se puede
programar para DS. En principio bastaría con un editor de texto simple como el bloc de notas
y luego utilizar alguna herramienta de generación de código como Make que relacione los
fuentes con el compilador, pero es preferible usar un editor inteligente que tenga utilidades
como coloreo de código, sugerencia de nombres reservados, vinculación con Make, que
pueda ejecutarse el código directamente desde el entorno sobre un emulador, o incluso que
con la ayuda de un script se envíe el compilado a la DS… Junto con devkitpro se incluyen
varios editores que ofrecen algunas de estas posibilidades. Quizás el más simple sea
Programmers Notepad 2 [16] que colorea el código según los estándares de C/C++ y es
posible compilar sin salir del entorno. El otro editor incluido es Visual HAM [18] que además
de colorear código y compilar, da la posibilidad de ejecutarlo en alguno de los emuladores
que estén instalados y configurados con el entorno. Otra opción más potente es usar Microsoft
Visual Studio C++ [18] ya sea en su versión completa o versión express. Este editor no viene
incluido con devkitpro y habrá que obtenerlo aparte. La versión express, Microsoft Visual
C++ Express Edition, es gratuita y se puede descargar desde la página de Microsoft. Para
configurarlo de manera que pueda usarse toda la potencia de esta herramienta con la librería
PAlib se adjunta un enlace [19] que indica los pasos a seguir. Una vez configurado
correctamente aparecerá un nuevo tipo de proyecto NintendoDS, Palib Application (figura 5):
Ahora el programador tendrá todas las facilidades de este editor como por ejemplo hacer uso
de la herramienta IntelliSense, o crear un script para que el compilado se envíe al dispositivo
de almacenamiento de DS.
Existe alguna utilidad interesante como por ejemplo instalar un servidor ftp en la
videoconsola al que poder conectarse con un cliente ftp vía wifi para enviar los ficheros
compilados sin tener que sacar de la DS el cartucho y la tarjeta de memoria. Esta cómoda
utilidad se llama DSFTP [20] y en su página web se puede encontrar una guía para hacerla
funcionar.
// Includes
#include <PA9.h> // Include para PA_Lib
// Función principal
int main(int argc, char ** argv)
{
PA_Init(); // Inicializa PA_Lib
PA_InitVBL(); // Inicializa un estándard VBL
return 0;
} // Fin main()
La primera línea es una directiva include y aparecerá siempre que vayamos a usar la librería
PAlib, PA9.h. Ya dentro del main llama a dos funciones de inicialización de la librería (se
ditinguen porque comienzan por PA_). La primera, PA_Init() es una inicialización general
y si no se pusiera no funcionaría correctamente PAlib. La siguiente línea es PA_InitVBL()
que hace referencia al VBL (Vertical Blank Line) que es el tiempo que tarda la pantalla en
refrescarse. Al inicializarlo permite al programa sincronizarlo con la pantalla, a 60 fps (frames
por segundo). Si no se hiciera esto la aplicación iría más rápido pudiendo incluso dejar de
funcionar. Por tanto, estas dos líneas serán siempre fundamentales junto con el primer
include.
Lo siguiente que aparece en el código es un bucle infinito que se corresponde con el ciclo de
ejecución de la aplicación. Hasta este punto la aplicación debe haberse encargado de
inicializar todos los datos y objetos necesarios, para que después se pueda acceder a este bucle
que se encargará básicamente de analizar los eventos que sucedan para actualizar la
información que se tenga que mostrar por la pantalla. En este caso dentro del bucle sólo existe
una línea que hace referencia a algo comentado anteriormente. Se trata de
Si ejecutáramos esta aplicación no aparecería nada, sólo una pantalla en negro. Esto es
correcto puesto que no se ha hecho uso de ninguna función que imprima por pantalla. Esta
será la próxima tarea.
3.2.3 Textos
En la documentación de la librería existe un módulo que hace referencia a mostrar textos por
pantalla, este es Text Output System. Aquí aparecen todas las funciones necesarias para esta
tarea. Un ejemplo de uso se puede ver en el siguiente código:
// Includes
#include <PA9.h> // Include para PA_Lib
// Función: main()
int main(int argc, char ** argv)
{
PA_Init(); // Inicializa PA_Lib
PA_InitVBL(); // Inicializa una estándard VBL
PA_InitText(1, 2);
PA_OutputSimpleText(1, 1, 2, "Hola Mundo DS");
return 0;
} // Fin main()
He aquí una aplicación que inicia la librería correctamente, para poder mostrar un texto
también hace falta iniciar dicho módulo con PA_InitText. Después hay que llamar a la
función PA_OutputSimpleText para que se muestre el texto por la pantalla. Sin
embargo, para usar estas funciones hace falta indicarles una serie de parámetros. La primera
De este modo se puede imprimir texto de una manera simple, pero existe otra función muy
importante que hace las veces de printf, esta es PA_OutputText (u8 screen, u16
x, u16 y, char *text, arguments...). Los primeros cuatro parámetros son
iguales pero además al final se pueden incluir una serie de argumentos que irán incluidos
dentro de la cadena text. Dependiendo del tipo que se vaya a mostrar se usarán unos
indicadores u otros:
• Integer: %d, Ejemplo: PA_OutputText(1,0,0,"valor: %d", 3);
• Float: PA_OutputText(1,0,0,"Float value : %f3", 4.678);
• String: PA_OutputText(1,0,0,"Hola %s", "mundo");
Del mismo modo que en los argumentos se pasan valores constantes, se podrían pasar
variables que contengan valores de cada tipo.
3.2.4 Entrada
Existen dos tipos de entrada en DS: pad (todos los botones) y stylus (puntero). Además hay
otras entradas, como por ejemplo un teclado implementado por PAlib que se muestra en
pantalla y se puede usar junto con el puntero de la videoconsola; también existe una
característica de reconocimiento de formas usando lo que se conoce en la librería como
PA_Graffiti que permite reconocer figuras predefinidas o configurar nuevas; otro tipo de
entrada de información sería el micrófono.
3.2.4.1 Pad
El estado del pad se almacena en una estructura homónima Pad. Dentro se puede diferenciar
por tres tipos de eventos que definen los estados posibles de los botones: Held, Released y
Newpress. Held es cuando una tecla está pulsada, permanecerá a 1 mientras se encuentre
en esa situación. Released se activará cuando el botón haya dejado de estar pulsado,
permaneciedo a 1 durante un frame. Por último NewPress valdrá 1 durante un frame cuando
se pulse la tecla. Un ejemplo sencillo sería:
if(Pad.Held.Up)
{
MoveUp();
}
if(Pad.Held.Down)
{
MoveDown();
}
Cuyo significado en un hipotético juego podría ser, si está pulsada la dirección arriba,
desplázate arriba, y si está pulsada la dirección abajo, desplázate hacia abajo.
3.2.4.2 Stylus
La lectura del Stylus es similar a la de Pad, existe una variable Stylus que almacena el
estado del puntero y se actualiza cada frame. Se pueden consultar los mismos estados: Held,
Released y NewPress. Además se puede conocer la posición del mismo consultando X e
Y de la variable Stylus. Un ejemplo de código es el siguiente:
if (Stylus.Held)
{
PA_SetSpriteXY(screen, sprite, Stylus.X, Stylus.Y);
}
Su significado es: si está el puntero pulsado sobre la pantalla, posiciona una imagen en la
posición donde se encuentra el puntero.
3.2.5 Imágenes
Por un lado hará falta mostrar textos por pantalla, por ejemplo, cuando queramos depurar será
fundamental para conocer los valores de las variables y el estado de la aplicación. Y por otra
parte, junto con los gráficos en tres dimensiones, el resto de cosas que se mostrarán serán
sprites, es decir, imágenes. Antes de comenzar es necesario conocer las características que
trae DS. Es capaz de mostrar 128 imágenes diferentes por cada pantalla, por tanto un total de
256 diferentes. Cada imagen puede ser girada horizontal/verticalmente, desplazada por toda la
pantalla, puede ser animada (actualizando la imagen de una secuencia), puede tener
transparencias o incluso hacerse un mosaico. Las imágenes también pueden ser rotadas o
aumentadas (o disminuidas). Existe una limitación y es que se pueden definir rotsets
(rotaciones/aumentos) y aplicarlos a las imágenes, pero no se pueden definir más de 32 rotsets
diferentes por pantalla. De tal modo se podrán rotar/aumentar las imágenes que sean ya que se
puede aplicar un mismo rotset a diferentes imágenes que se transformarán igual, pero sólo de
32 maneras distintas.
Si a partir de ahora trabajaremos pintando imágenes en pantalla, hará falta indicar en qué
posición de esta se va a mostrar. Por tanto, será necesario conocer las posibles coordenadas de
esta. Como se puede apreciar en la figura 6:
Como se puede apreciar, las coordenadas posibles en cada pantalla serán de 0 a 255 pixeles en
acho y de 0 a 191 pixeles en alto; siendo (0,0) la posición más arriba a la izquierda y
(255,191) la más abajo a la derecha.
También es necesario conocer los tres diferentes modos de colores que existen en DS para los
sprites:
• Paleta de 16 colores: muy usada en GBA.
• Paleta de 256 colores: usa más memoria.
• Imágenes de 16 bits: Sin paleta, no se suele usar mucho.
Lo más común es usar paleta de 256 colores ya que es el término medio con mejor
rendimiento.
Además las imágenes deben tener unos tamaños específicos, estos son: 8x[8,16,32] (esto
significa: 8x8, 8x16, 8x32), 16x[8,16,32], 32x[8,16,32,64] y 64x[30, 64]. En el caso de que la
imagen que se vaya a usar tenga unas proporciones diferentes, habría que encapsularla a la
siguiente superior que sea reconocida.
En muchas ocasiones ocurre que no interesa que la imagen se vea completamente sino que
sólo se muestre un contenido interior y el resto no se vea, mostrándose hasta el perfil de la
figura. Para esto será posible definir que un color en concreto sea transparente.
Todas estas cosas a tener en cuenta sobre imágenes pueden hacer que haya que dedicar
bastante tiempo retocando y ajustando e incluso perder tiempo con errores que se creen que
tienen su origen en el código y a lo mejor el problema era de compatibilidad de proporciones
o de paleta de colores. Por este motivo el creador de la librería PAlib también ha dispuesto de
una herramienta gráfica para transformar imágenes a tipos compatibles. Se trata de PAGfx. A
continuación unos ejemplos de carga y transformaciones de sprites.
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.h"
#include "gfx/all_gfx.c"
int main(void){
PA_LoadSpritePal(
0, // Pantalla
0, // Número de paleta
(void*)sprite0_Pal); // Nombre de paleta
PA_CreateSprite(
0, // Pantalla
0, // Número de sprite
(void*)vaisseau_Sprite, // Nombre de sprite
OBJ_SIZE_32X32, // Tamaño de sprite
1, // Modo de color 256
Las dos líneas de include, que no son para incluir la librería PAlib, tratan de cargar la imagen
y la librería previamente convertidas a código C con la herramienta antes comentada PAGfx.
Después de la inicialización se llama a una nueva función PA_LoadSpritePal que carga
la paleta de la imagen y su uso es el siguiente: PA_LoadSpritePal (u8 screen, u8
palette_number, void *palette). En el caso del código escrito lo que hace es
cargar una paleta en la pantalla 0, en la primera paleta (la número 0) y la paleta cargar está en
la dirección sprite0_Pal. La otra función que hace posible mostrar la imagen es
PA_CreateSprite (u8 screen, u8 obj_number, void *obj_data, u8
obj_shape, u8 obj_size, u8 color_mode, u8 palette, s16 x, s16
y). Lo que hace esta función en el ejemplo es lo siguiente: crea un sprite en la pantalla 0 (la
pantalla inferior donde ya está cargada la paleta, si no existiera esta correspondencia no se
mostraría la imagen ya que cada pantalla maneja paletas diferentes), el segundo parámetro
indica que el sprite se asociará con el índice 0 (de los 128 posibles, 0-127) el cual lo
representará para posibles futuras transformaciones del mismo, además de representar el nivel
de profundidad al que se encuentra. Esto es que cuanto más pequeño es el valor del sprite, se
encuentra por delante de los demás. El tercer parámetro es el puntero hacia la imagen
previamente convertida, después se le indica el tamaño de la imagen (lo mejor es usar la
macro como en el ejemplo que engloba los dos parámetros reales que hay que pasarle), a
continuación se indica el modo de color siendo 0 para 16 colores o 1 para 256 (normalmente
será 1), después el número de paleta donde almacenamos la paleta correspondiente a esta
imagen (en este caso 0), para terminar se indica la coordenada donde se representará la
imagen.
Con esto se muestra la imagen por pantalla, lo próximo será desplazarla por ella atendiendo a
eventos del usuario.
// Los includes
#include <PA9.h>
// PAGfx Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
//Función principal
int main(void) {
//Inicialización de PAlib
PA_Init();
PA_InitVBL();
// Carga de 16 sprites
u8 i = 0;
for (i = 0; i < 16; i++)
PA_CreateSprite( 0, i,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1,
0, i << 4, i << 3);
while(1)
{
// Usa la función PA_MoveSpite sobre todos los sprites
for (i = 0; i < 16; i++) PA_MoveSprite(i);
// La función PA_MoveSprite comprueba si está
// pulsado un sprite con el puntero, y se mueve con él.
PA_WaitForVBL();
}
return 0;
}
El comienzo de este programa es muy parecido del anterior. Se inicializa PAlib y se carga la
paleta de imágenes. Y ahora en lugar de crear un sprite se crean 16 haciendo uso de la misma
imagen pero en posiciones diferentes.
El operador >> se utiliza en C para desplazar la cantidad de la izquierda el número de bits que
se le indique en el operando derecho, lo que se corresponde con dividir entre potencias de dos
la cantidad indicada. Si en cambio se usa el operador << la operación que se realiza es
introducir un bit por la derecha, por tanto se multiplica la cantidad por potencias de dos. En
este código en cada iteración se multiplica i por 24, 16, para obtener la coordenada x y se
multiplica y por 23, 8, para la coordenada en y. Como i va desde 0 a 15 lo que resulta es una
línea diagonal descendente desde el eje de coordenadas en (0,0) (hay que recordar que esto se
corresponde con la posición arriba a la izquierda) hasta la coordenada (15*16,15*8),
(240,120).
PA_SetSpriteXY
Con esta función habrá más libertad para desplazar los sprites a la coordenada que más
convenga, en lugar de cómo sucedía con la función PA_MoveSprite que siempre se
desplazaba al lugar donde estuviera el puntero. La función a utilizar será la siguiente
PA_SetSpriteXY (u8 screen, u8 sprite, s16 x, s16 y). Es necesario
indicarle la pantalla donde se encuentra el sprite, el identificador de este y la coordenada x e
y a donde se quiera mover. Hay que tener en cuenta que la posición del sprite se corresponde
con la esquina superior izquierda de este, no con su centro. Para recuperar las coordenadas de
un sprite se usan las macros PA_GetSpriteX(screen, obj) y
PA_SetSpriteY(screen, obj, y).
Como ejemplo de uso, con este código el sprite se desplazará en función de la pulsación del
cursor:
#include <PA9.h>
// PAGfx Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
//Función main
int main(void){
PA_InitText(0,0);
PA_LoadSpritePal( 0, // Pantalla
0, // Número de paleta
(void*)sprite0_Pal); // Nombre de paleta
PA_WaitForVBL();
}
return 0;
}
Si, por ejemplo, se quisiera desplazar la imagen al lugar donde se pulse con el puntero, habría
que cambiar la línea PA_SetSpriteXY(0, 0, x, y); por esta
Rotación
Este es el código del ejemplo:
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
int main(void){
PA_Init();
PA_InitVBL();
// Carga la paleta
PA_LoadSpritePal(
0, // Pantalla
0, // Número de paleta
(void*)sprite0_Pal); // Nombre de paleta
// Carga el sprite
PA_CreateSprite(
0, 0,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 50, 50);
while(1)
{
++angle; // modifica el ángulo
// Limita el rango de 0-511.
// Funciona sólo con 1, 3, 7, 15, 31... (2^n - 1)
angle &= 511;
PA_WaitForVBL(); // Sincronización
}
return 0;
}
El operador & se utiliza para calcular el módulo de una manera mucho más eficiente que con
el operador %. El operador % se utiliza para obtener el módulo o lo que es lo mismo, el resto
de la división. Sin embargo, utilizando el operador &, se realiza la operación a nivel de bit
con la mejora en la velocidad de cálculo que esto supone. Sin embargo, tiene como restricción
que el número que se le pase en el lado derecho debe ser una potencia de dos y además en
lugar de pasarle ese número en concreto, por ejemplo b, se le debe pasar b-1.
Zoom
Código que muestra imágenes rotando:
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
int main(void){
PA_Init();
PA_InitVBL();
// Carga la paleta
PA_LoadSpritePal(
0, // Pantalla
0, // Número de paleta
(void*)sprite0_Pal); // Nombre de paleta
// Carga el sprite
PA_CreateSprite(
0, 0,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 50, 50);
while(1)
{
// Modifica el ángulo según las teclas
PA_WaitForVBL(); // Sincronización
}
return 0;
}
Ejemplo muy similar al anterior debido a que un rotset puede representar tanto zoom como
rotaciones, por tanto sólo hay que modificar el tipo de transformación que se aplica a la
imagen. Sin embargo, es necesario comentar algunos detalles que no aparecían anteriormente.
Ahora usamos una variable llamada zoom que se va modificando para aplicar un zoom
distinto en cada ocasión. Se inicializa con 256 ya que esta cifra se corresponde al tamaño
normal, es decir 100% de su tamaño. Si la cifra es mayor se le aplicará una reducción, por
ejemplo 512 se corresponde con el 50% del tamaño original. Por consiguiente, si la cifra es
inferior se aumenta el tamaño, con un valor de 128 la imagen se muestra al 200% de su
tamaño.
El problema de aumentar una imagen es que si esta ocupa por ejemplo 32x32 píxeles, al hacer
zoom se pasará de sus límites y se saldrá de los límites que tiene establecidos. Por ese motivo
existe la función PA_SetSpriteDblsize(screen, obj, dblsize), esta permite
habilitar la opción para que la imagen pueda abarcar el doble de su tamaño. El efecto se puede
comprobar en el ejemplo ya que esta función se ha aplicado solamente a una de las imágenes.
Zoom y rotación
Para esta ocasión no considero necesario copiar aquí ningún ejemplo aunque si irá incluido
dentro de los códigos fuentes por si resulta necesario. La función usada para realizar estas dos
operaciones es PA_SetRotset (u8 screen, u8 rotset, s16 angle, u16
zoomx, u16 zoomy), que consiste en una combinación de las dos vistas anteriormente
donde se puede especificar el ángulo de rotación y el zoom horizontal y vertical del rotset.
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
int main(void){
PA_LoadSpritePal(
0, // Pantalla
0, // Número de paleta
(void*)sprite0_Pal); // Nombre de paleta
return 0;
}
3.2.5.5 Transparencias
También conocido como alpha-blending. PAlib usa el hardware de Nintendo DS para llevar a
cabo esta tarea. Existe una limitación y es que se puede aplicar la transparencia a todas las
imágenes que sean pero sólo se podrá disponer de un grado de transparencia, es decir, que
todas las imágenes a las que le afecte esta modificación se verán con la misma transparencia.
Lo primero que hay que realizar para poder dar transparencia a un sprite es habilitar dicha
opción con PA_SetSpriteMode(screen, sprite, obj_mode). En el tercer
parámetro se establece el modo siendo 0 para modo normal, 1 para transparencia y 2 para
modo ventana.
Una vez activado, el sprite no se mostrará transparentado todavía. Antes es necesario activar
el sistema de efectos especiales para DS, estableciendo el modo de transparencia de nuevo
con PA_EnableSpecialFx(Screen, SFX_ALPHA (Alpha blending mode),
0 (leave to 0 for now), SFX_BG0 | SFX_BG1 | SFX_BG2 | SFX_BG3
| SFX_BD). Por último se establece el nivel de alpha al sprite con PA_SetSFXAlpha.
Comentar además que este ejemplo no funciona bien con los emuladores que he probado, es
necesario hacerlo funcionar en el hardware de la videoconsola Nintendo DS. El código de
ejemplo es el siguiente:
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
int main(void){
PA_LoadSpritePal(
0, // Pantalla
0, // Número de paleta
(void*)sprite0_Pal); // Nombre de paleta
PA_SetSpriteMode(
0, // Pantalla
0, // Sprite
1); // Alphablending
// Habilita el alpha-blending
PA_EnableSpecialFx(
0, // Pantalla
SFX_ALPHA, // Modo alpha blending
0, // Nada
SFX_BG0 | SFX_BG1 | SFX_BG2 | SFX_BG3 | SFX_BD); // Lo normal
PA_WaitForVBL();
}
return 0;
}
3.2.5.6 Profundidad
Este es también un aspecto importante ya que si existen varias imágenes superpuestas, ¿cuál
se verá por encima? Con el concepto de profundidad se podrá establecer estar prioridad.
Existen dos tipos de prioridades en DS:
• Prioridad de sprite: una imagen con un número de sprite menor se verá por encima de otro
sprite con un número mayor.
• Prioridad de fondo: Está por encima de la prioridad de sprite. Por defecto todos los sprites
están en el fondo número 0. Se puede establecer que un sprite se sitúe en frente de otro
fondo, 0-3. Una imagen puesta en el fondo número 2 estará detrás de todos los sprites con
un fondo de prioridad 0 ó 1, y por delante de todos los sprites con un fondo de prioridad 3.
PA_Rand()
Esta es la función más sencilla, genera un número aleatorio pero de un tamaño muy grande,
por tanto no suele usarse mucho. Las siguientes funciones evitarán tener que usar esta función
y posteriores transformaciones sobre su resultado.
PA_RandMax(max)
A esta función se le puede pasar un valor máximo de manera que si por ejemplo se le pasa el
valor de 6, devolverá un valor aleatorio entre 0 y 6 ambos inclusive.
PA_RandMinMax(min,max)
Con PA_RanMinMax se puede establecer un valor mínimo y otro máximo inclusives para el
valor devuelto.
PA_InitRand()
Llamando a esta función se podrá inicializar la semilla de números aleatorios con la hora y
fecha en el momento de su invocación. Usándola una vez antes del bucle principal se podrán
conseguir que cada ejecución de la aplicación genere números distintos con mayor
probabilidad.
Usando s32, variable de 32 bit, nosotros podremos reservar los últimos 8 bits para almacenar
la parte decimal y los 24 restantes para la parte entera. Los 8 bits pueden tener valores entre 0
y 255, por tanto la precisión que se puede llegar a tener es de 1/256 la cual no está nada mal.
Por tanto, en el punto fijo, un valor de 256 significa la unidad, 512 significa 2, 1024 significa
4… Del mismo modo para obtener valores decimales, 128 se corresponde con 0.5.
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
int main(void){
PA_InitText(0, 0);
PA_LoadSpritePal(0, 0, (void*)sprite0_Pal);
PA_CreateSprite(
0, 0, (void*)vaisseau_Sprite, OBJ_SIZE_32X32, 1, 0, 0, 0);
PA_CreateSprite(
0, 1, (void*)vaisseau_Sprite, OBJ_SIZE_32X32, 1, 0, 0, 64);
PA_CreateSprite(
0, 2, (void*)vaisseau_Sprite, OBJ_SIZE_32X32, 1, 0, 0, 128);
PA_WaitForVBL();
}
return 0;
}
Se observan las tres variables de tipo s32 que controlan las velocidades de las imágenes:
speed1, speed2 y speed3. Sus valores son 256, 128 y 64 respectivamente que se corresponden
con velocidades de 1, 0.5 y 0.25 píxeles por frame. Después dentro del bucle principal se
actualizan las posiciones en x según la velocidad que tiene cada imagen. Por último se
posicionan los sprites realizando una conversión. Se hace uso la operación >> 8 para dicha
tarea ya que como se ha comentado previamente esto es equivalente a dividir entre 28, 256.
He aquí que 256 se corresponda con 1, ya que se realiza la operación 256/256; 128 es 0.5 ya
que 128/256 es dicha cantidad; y por último 64/256 es igual a 0.25. Siendo esto la parte
importante porque realizar la división entre 256 es lento mientras que realizando la operación
>>8 que al trabajar a nivel de bit, se convierte en una operación mucho más rápida. Si se
quisiera convertir de la cantidad transformada, es decir pasar por ejemplo de 256 a 1 o 128 a
0.5, será necesario realizar la operación inversa <<8 que multiplicará por 28, 256.
Ángulos en PAlib
Algo a lo que se ha hecho mención anteriormente es que los ángulos en PAlib no son como
los cotidianos. El rango en DS va desde 0º hasta 511º y en sentido contrario a las agujas del
reloj. El orden es el siguiente, 0º es el lado derecho, 128 apunta hacia arriba, 256 es el lado
izquierdo y 384 se corresponde con abajo. Se puede ver ilustrado en la figura 7.
PAlib ofrece una función que simplifica el trabajar con estos ángulos, se trata de
PA_GetAngle (s32 startx, s32 starty, s32 targetx, s32 targety)
y devuelve el ángulo formado entre la línea horizontal y la línea compuesta por los dos puntos
(startx, starty) y (targetx, targety).
// Includes
#include <PA9.h>
#include "gfx/vaisseau.raw.c"
#include "gfx/master.pal.c"
int main(void){
PA_Init();
PA_InitVBL();
PA_LoadSpritePal(0, 0, (void*)master_Palette);
PA_CreateSprite(
0, 0,(void*)vaisseau_Bitmap,
OBJ_SIZE_32X32,1, 0, 128-16, 96-16);
while(1)
{
PA_OutputText(
1, 5, 10, "Angulo : %d ",
PA_GetAngle(128, 96, Stylus.X, Stylus.Y));
PA_WaitForVBL();
}
return 0;
}
Muestra en la pantalla inferior una imagen de una nave y en la pantalla superior se muestra el
grado que existe entre la línea horizontal que atraviesa la nave y la línea que se forma desde el
centro de la nave con la pulsación del puntero sobre la pantalla.
Senos y Cosenos
La primera diferencia entre estas funciones es que no devuelven un valor entre -1 y 1, sino
entre -256 y 256. De esta manera es mucho más eficiente ya que hace uso de números
decimales con punto fijo. A continuación se muestra una imagen que representa esta
situación:
Para obtener el seno y el coseno a partir de un ángulo se hace uso de las funciones
PA_Sin(angle) y PA_Cos(angle) respectivamente.
Ejemplo de trayectoria
Este ejemplo muestra una nave en el centro de la pantalla que puede desplazarse hacia
delante, atrás y girar a derecha e izquierda. Aquí está el código:
// Includes
#include <PA9.h>
// PAGfxConverter Include
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
int main(void){
PA_Init();
PA_InitVBL();
PA_LoadSpritePal(0, 0, (void*)sprite0_Pal);
while(1)
{
angle += Pad.Held.Left - Pad.Held.Right;
// Gira la nave en la dirección correcta
PA_SetRotsetNoZoom(0, 0, angle);
PA_WaitForVBL();
}
return 0;
}
Primero se crea una imagen y se le vincula a un rotset para poder girarla. Después se
inicializan las variables que se usarán, en este caso las posiciones x e y, y el ángulo de
dirección. Las posiciones x e y son variables de tipo s32 por tanto estamos usando decimales
con punto fijo por lo que hay que hacer uso de la operación << 8.
3.2.7 Sonido
El sonido se presenta como una parte importante en cualquier aplicación gráfica ya que sin
sonido quedaría incompleta quedando por muy cuidados que estuvieran el resto de detalles.
Ya sea desde acompañar el juego por una música que revele el estado de las situaciones
sumergiendo al usuario en el ambiente de lo que está viendo, o bien ayudando a la trama con
efectos de sonidos que acompañan a las acciones o eventos que se produzcan. Tal como lo
representa la palabra audiovisual, la parte gráfica junto con el sonido se complementan la una
con la otra para conseguir un mismo objetivo, transmitir sensaciones al usuario. Por tanto, se
convierte el audio en una parte imprescindible que no puede dejar de mencionarse.
En PAlib existen dos métodos diferentes para cargar sonidos en la salida de audio: el formato
raw para reproducir sonido wav, usado sobre todo para efectos especiales debido a que ocupa
mucho espacio; y la reproducción mod que es perfecta para sonido de fondo en bucle ya que
ocupa muy poco espacio.
La conversión a formato raw debe llevarse a cabo usando algún programa. Uno de los más
sencillos es Switch que en su versión gratuita permite la conversión entre varios formatos
incluido raw. Basándome en este conversor, además de seleccionar un formato de salida .raw,
es posible especificar algunos criterios de codificación. Para obtener archivos de poco tamaño
con una calidad aceptable se pueden dejar estas opciones:
Para que la conversión sea válida es necesario dejar el formato de 8 bit signed, sin embargo
las otras dos opciones se pueden mejorar para obtener una mejor calidad. Por ejemplo,
poniendo mayor cantidad de muestreo (sample) o incluso establecer sonido estéreo.
Para poder reproducir un archivo de sonido raw en una aplicación es necesario que este se
aloje en la carpeta data que está al mismo nivel que la carpeta source, en caso de que no exista
será necesario crearla. El código siguiente reproduce un sonido al pulsar el botón A:
// Includes
#include <PA9.h> // Include para PA_Lib
#include "saberoff.h" // Include para el sonido
//(se aloja en la carpeta data en formato .raw)
// Función: main()
int main(int argc, char ** argv)
{
PA_Init(); // Inicializa PA_Lib
PA_InitVBL(); // Inicializa un standard VBL
PA_InitText(0, 0);
// Bucle infinito
while (1)
{ // Reproduce el sonido si A ha sido pulsado
if (Pad.Newpress.A)
PA_PlaySimpleSound(0, saberoff);
PA_WaitForVBL();
}
return 0;
} // Fin de main()
Lo primero importante que se hace antes de comenzar las instrucciones es incluir el archivo
raw con #include "saberoff.h". Después cuando se quiera reproducir el sonido
bastará con usar su nombre ya que se ha establecido como un puntero al contenido de dicho
fichero. A continuación es necesario inicializar el sistema de sonido de PAlib (sirve también
para la reproducción de archivos mod) estableciendo el tipo de archivo por defecto a muestreo
11025 y 8 bit signed format. Por último queda efectuar la acción de reproducir el sonido en el
momento que sea necesario, en nuestro caso cuando se pulse el botón A. Para este fin se llama
a la función PA_PlaySimpleSound(PA_Channel, sound) a la que se le indica un
canal de salida de 0-7 y un archivo de sonido previamente incluido en el código. Pueden
usarse hasta 8 canales de reproducción al mismo tiempo. Por defecto los otros 8 canales
disponibles están reservados para la reproducción de archivos mod, aunque esto es
modificable.
// Includes
#include <PA9.h> // Include para PA_Lib
#include "modfile.h" // Include el archivo mod (el archivo .mod se aloja
// en la carpeta data)
// Función: main()
int main(int argc, char ** argv){
PA_Init();
PA_InitVBL();
PA_WaitForVBL();
}
return 0;
} // Fin de main()
Es similar al ejemplo de la sección 3.2.7.1, sin embargo, es necesario usar otra función para la
reproducción. En este caso se llama a PA_PlayMod(mod_snd). Una cosa que hay que
comentar es que el archivo mod puede tener hasta 16 canales, pero es recomendable usar 8
para dejar los 8 restantes libres para efectos de sonido.
#include <PA9.h>
// Función Main
int main(void)
{
// Inicialización de PAlib
PA_Init();
PA_InitVBL();
// Inicializa el texto
PA_InitText(1, 0);
while(1)
{
// Día, Mes y Año
PA_OutputText(
1, 2, 10, "%02d/%02d/%02d", PA_RTC.Day, PA_RTC.Month,
PA_RTC.Year);
// Hora HH:MM SS
PA_OutputText(1, 2, 12, "%02d:%02d %02d segundos", PA_RTC.Hour,
PA_RTC.Minutes, PA_RTC.Seconds);
PA_WaitForVBL();
}
return 0;
}
PA_RTC es una estructura que se actualiza cada frame la cual contiene toda la información
sobre la fecha y hora actual. Estas son las variables que contiene:
• PA_RTC.Day, de 1 a 31.
• PA_RTC.Month, de 1 a 12.
• PA_RTC.Year, de 00 (para 2000) a 99 (para 2099).
• PA_RTC.Hour, de 0 a 23.
• PA_RTC.Minutes, de 0 a 59.
• PA_RTC.Seconds, de 0 a 59.
#include <PA9.h>
// Función Main
int main(void)
{
// Inicialización de PAlib
PA_Init();
PA_InitVBL();
// Inicializa el texto
PA_InitText(1, 0);
while(1)
{
// Nombre de usuario, cumpleaños
PA_OutputText(
1, 2, 10, "Nombre usuario : %s, %02d/%02d",
PA_UserInfo.Name, PA_UserInfo.BdayDay,
PA_UserInfo.BdayMonth);
// Alarma
PA_OutputText(
1, 2, 12, "Alarma : %02d:%02d", PA_UserInfo.AlarmHour,
PA_UserInfo.AlarmMinute);
// Mensaje especial
PA_OutputText(1, 2, 16, "Mensaje : %s", PA_UserInfo.Message);
PA_WaitForVBL();
}
return 0;
}
caso de detectar que se han cerrado las tapas, pausa la consola y devuelve un 1. Aquí se puede
ver un ejemplo:
#include <PA9.h>
// Función Main
int main(void)
{
// Inicialización de PAlib
PA_Init();
PA_InitVBL();
while (1)
{
PA_CheckLid(); // Comprueba si está cerrada
PA_WaitForVBL();
}
return 0;
}
3.3 Programación 3D
Hasta ahora se ha visto la mayor parte de herramientas que dispone un desarrollador para
construir aplicaciones en Nintendo DS. La parte que viene a continuación trata sobre las
posibilidades que ofrecen las librerías ya comentadas para trabajar con escenarios en 3D. Se
verá cómo poder inicializar estas funciones, pintar elementos básicos como quads y
triángulos, aplicar transformaciones 3D, manejar texturas…
// Ejemplo inicializar 3D
#include <PA9.h>
// Función Main
int main(void)
{
// Inicialización de PAlib
PA_Init();
PA_InitVBL();
// Habilita antialias
glEnable(GL_ANTIALIAS);
// Orienta la cámara
while (1)
{
// Push de la matriz activa (salva el estado)
glPushMatrix();
DrawGLScene();
PA_WaitForVBL();
}
return 0;
}
int DrawGLScene(void)
{
// Carga la matriz identidad sobre la matriz activa
glLoadIdentity();
return TRUE;
}
Comienza incluyendo la librería PAlib como siempre se ha hecho, ya que se seguirán usando
algunas funciones de ésta. No es necesario incluir la librería libnds porque ya se hace de
manera implícita al cargar PAlib. Después se declara una función llamada DrawGLScene()
que contendrá en los ejemplos siguientes la escena 3D a pintar. Dentro de la función principal
main se inicializa PAlib como siempre. A continuación se inicializan las funciones 3D
llamando a PA_Init3D(). En su interior se inicializa lo básico para poder usar las
funciones 3D pero será necesario establecer más adelante una serie de características de
configuración que serán distintas según las circunstancias. La función glEnable se encarga
de habilitar propiedades, en este caso activa un filtro antialias que mejora la visualización, por
ejemplo suavizando los contornos de las figuras. Con la función glMatrixMode se
establece a cuál de las matrices que utiliza openGL se le aplicarán las transformaciones
resultantes de las funciones que aparezcan a continuación de dicha llamada, es decir, que
mientras no se establezca una matriz con glMatrixMode, esta no podrá ser modificada. Las
matrices más usadas de openGL son dos: GL_PROJECTION y GL_MODELVIEW. La primera
es la responsable de establecer la perspectiva de nuestra escena. La segunda matriz contiene
3.3.2 Polígonos
Cuando ya se ha inicializado convenientemente la librería openGl, el sistema está dispuesto
para dibujar elementos 3D y poder mostrarlos. Por tanto, sólo será necesario modificar la
función DrawGLScene() ya que es la que se encarga en cada iteración del bucle infinito
de pintar sobre la pantalla los elementos deseados:
glTranslatef(-1.5f,0.0f,-3.0f);
// Dibuja triángulos
glBegin(GL_TRIANGLES);
glColor3f(1.0f,0.0f,0.0f); // Establece el color de
// vértice a Rojo
glVertex3f( 0.0f, 1.0f, 0.0f); // Vértice superior
glColor3f(0.0f,1.0f,0.0f); // Establece el color de
// vértice a Verde
glVertex3f(-1.0f,-1.0f, 0.0f); // Vértice inferior
// izquierdo
glColor3f(0.0f,0.0f,1.0f); // Establece el color de
// vértice a Azul
glVertex3f( 1.0f,-1.0f, 0.0f); // Vértice inferior derecho
// Fin Triángulo
glEnd();
// Dibuja un Quad
glBegin(GL_QUADS);
glVertex3f(-1.0f, 1.0f, 0.0f); // Vértice superior izquierdo
glVertex3f( 1.0f, 1.0f, 0.0f); // Vértice superior derecho
glVertex3f( 1.0f,-1.0f, 0.0f); // Vértice inferior derecho
glVertex3f(-1.0f,-1.0f, 0.0f); // Vértice inferior izquierdo
// Fin Quad
glEnd();
return TRUE;
}
OpenGL trabaja cargando sobre sus matrices las transformaciones que se van solicitando.
Cada vez que se realiza una nueva operación, ya sea pintar un polígono, rotar la escena o
cualquier otra tarea, esta se aplica sobre el resto de modificaciones anteriores. Por este motivo
lo que se realiza al comienzo de la función es establecer como matriz activa la matriz que
carga los modelos de la escena junto con sus transformaciones y resetearla, es decir, se carga
la matriz identidad sobre ella perdiendo su contenido anterior. Así queda lista para trabajar
con ella desde el principio. La siguiente instrucción se corresponde con void
glTranslatef(float x, float y, float z) que como su nombre indica
realiza una translación sobre los elementos que se pinten a continuación de esta instrucción.
Si no se pusiera esta instrucción, lo que se pintara se haría sobre el punto (0,0) que se
encuentra en el centro de la pantalla. Los parámetros que se le pasan a esta función le indican
la cantidad de desplazamiento en x (horizontal), y (vertical) y z (profundidad)
respectivamente. Siendo las coordenadas positivas las que desplazan hacia la derecha, arriba y
hacia la pantalla en cada caso. Para este ejemplo, la translación es en 1.5 unidades hacia la
izquierda y 6 unidades alejadas de la pantalla con respecto del usuario.
3.3.3 Rotación
En este caso además de modificar la función DrawGLScene, también se han creado dos
variables globales para controlar las rotaciones. Estas guardarán un valor que se modificará
tras cada iteración para que dé la sensación de que las figuras están girando cuando en
realidad se están volviendo a pintar con distintas inclinaciones.
{
// Activa la matriz de modelos de escena
glMatrixMode(GL_MODELVIEW);
// Dibuja triángulos
glBegin(GL_TRIANGLES);
glColor3f(1.0f,0.0f,0.0f); // Establece el color de
// vértice a Rojo
glVertex3f( 0.0f, 1.0f, 0.0f); // Vértice superior
glColor3f(0.0f,1.0f,0.0f); // Establece el color de
// vértice a Verde
glVertex3f(-1.0f,-1.0f, 0.0f); // Vértice inferior
// izquierdo
glColor3f(0.0f,0.0f,1.0f); // Establece el color de
// vértice a Azul
glVertex3f( 1.0f,-1.0f, 0.0f); // Vértice inferior derecho
// Fin Triángulo
glEnd();
return TRUE;
}
Para poder dibujar objetos 3D es necesario representar las caras que lo forman, vértice por
vértice. Esto quiere decir que para construir una pirámide cuadrangular hace falta pintar
cuatro caras, prescindiendo de la cara inferior que serviría de base cerrando la figura. Si la
figura a representar es un cubo serán necesarias las seis caras que forman un dado. Para la
representación de cada cara se usa el tipo de estructura más conveniente de las vistas
anteriormente. Recordemos que básicamente en este implementación para DS existen dos:
GL_TRIANGLES y GL_QUADS de 3 y 4 vértices respectivamente. Las otras dos que existen
GL_TRIANGLE_STRIP y GL_QUAD_TRIP son agrupaciones de elementos de las otras dos.
A continuación el código del ejemplo que es una extensión del anterior ya que donde había un
triángulo ahora se dibuja una pirámide y donde aparecía un cuadrado ahora hay un cubo.
Existe otra modificación, en lugar de girar el cubo sobre un eje ahora lo hace sobre dos.
// Dibuja triángulos
glBegin(GL_TRIANGLES);
glColor3f(1.0f,0.0f,0.0f); // Rojo
glVertex3f( 0.0f, 1.0f, 0.0f); // Vértice superior (Frente)
glColor3f(0.0f,1.0f,0.0f); // Verde
glVertex3f(-1.0f,-1.0f, 1.0f); // Vértice inferior
// izquierdo (Frente)
glColor3f(0.0f,0.0f,1.0f); // Azul
glVertex3f( 1.0f,-1.0f, 1.0f); // Vértice inferior derecho
// (Frente)
glColor3f(1.0f,0.0f,0.0f); // Rojo
glVertex3f( 0.0f, 1.0f, 0.0f); // Vértice superior
// (Derecha)
glColor3f(0.0f,0.0f,1.0f); // Azul
glVertex3f( 1.0f,-1.0f, 1.0f); // Vértice inferior
// izquierdo (Derecha)
glColor3f(0.0f,1.0f,0.0f); // Verde
glVertex3f( 1.0f,-1.0f, -1.0f); // Vértice inferior derecho
// (Derecha)
glColor3f(1.0f,0.0f,0.0f); // Rojo
glVertex3f( 0.0f, 1.0f, 0.0f); // Vértice superior (Atrás)
glColor3f(0.0f,1.0f,0.0f); // Verde
glVertex3f( 1.0f,-1.0f, -1.0f); // Vértice inferior
// izquierdo (Atrás)
glColor3f(0.0f,0.0f,1.0f); // Azul
glVertex3f(-1.0f,-1.0f, -1.0f); // Vértice inferior derecho
// (Atrás)
glColor3f(1.0f,0.0f,0.0f); // Rojo
glVertex3f( 0.0f, 1.0f, 0.0f); // Vértice superior
// (Izquierda)
glColor3f(0.0f,0.0f,1.0f); // Azul
glVertex3f(-1.0f,-1.0f,-1.0f); // Vértice inferior
// izquierdo (Izquierda)
glColor3f(0.0f,1.0f,0.0f); // Verde
glVertex3f(-1.0f,-1.0f, 1.0f); // Vértice inferior derecho
// (Izquierda)
// Fin Triángulo
glEnd();
// Dibuja un Quad
glBegin(GL_QUADS);
glColor3f(0.0f,1.0f,0.0f); // Verde
glVertex3f( 1.0f, 1.0f,-1.0f); // Vértice superior derecho
// (Arriba)
glVertex3f(-1.0f, 1.0f,-1.0f); // Vértice superior
// izquierdo (Arriba)
glVertex3f(-1.0f, 1.0f, 1.0f); // Vértice inferior
// izquierdo (Arriba)
glVertex3f( 1.0f, 1.0f, 1.0f); // Vértice inferior derecho
// (Arriba)
glColor3f(1.0f,0.5f,0.0f); // Color naranja
glVertex3f( 1.0f,-1.0f, 1.0f); // Vértice superior derecho
// (Abajo)
glVertex3f(-1.0f,-1.0f, 1.0f); // Vértice superior
// izquierdo (Abajo)
glVertex3f(-1.0f,-1.0f,-1.0f); // Vértice inferior
// izquierdo (Abajo)
glVertex3f( 1.0f,-1.0f,-1.0f); // Vértice inferior derecho
// (Abajo)
glColor3f(1.0f,0.0f,0.0f); // Rojo
glVertex3f( 1.0f, 1.0f, 1.0f); // Vértice superior derecho
// (Frente)
glVertex3f(-1.0f, 1.0f, 1.0f); // Vértice superior
// izquierdo (Frente)
glVertex3f(-1.0f,-1.0f, 1.0f); // Vértice inferior
// izquierdo (Frente)
glVertex3f( 1.0f,-1.0f, 1.0f); // Vértice inferior derecho
// (Frente)
glColor3f(1.0f,1.0f,0.0f); // Amarillo
glVertex3f( 1.0f,-1.0f,-1.0f); // Vértice superior derecho
// (Atrás)
glVertex3f(-1.0f,-1.0f,-1.0f); // Vértice superior
// izquierdo (Atrás)
glVertex3f(-1.0f, 1.0f,-1.0f); // Vértice inferior
// izquierdo (Atrás)
glVertex3f( 1.0f, 1.0f,-1.0f); // Vértice inferior derecho
// (Atrás)
glColor3f(0.0f,0.0f,1.0f); // Azul
glVertex3f(-1.0f, 1.0f, 1.0f); // Vértice superior derecho
// (Izquierda)
glVertex3f(-1.0f, 1.0f,-1.0f); // Vértice superior
// izquierdo (Izquierda)
glVertex3f(-1.0f,-1.0f,-1.0f); // Vértice inferior
// izquierdo (Izquierda)
return TRUE;
}
3.3.5 Texturas
Ya se han visto las funcionalidades básicas que aporta esta implementación de openGL, con
las cuales técnicamente se podría hacer cualquier figura y aplicarle cualquier color. Sin
embargo, a la hora de mostrarlo dejaría bastante que desear ya que estéticamente no
impresionarían demasiado esas formas poligonales con colores tan planos, dando la sensación
de algo tosco y no llamaría la atención lo cual suele ser el gran objetivo de cualquier producto
audiovisual. Por tanto, quedarían dos aspectos visuales más que mencionar. Uno sería cómo
cargar en nuestras aplicaciones modelos 3D realizados con software de diseño especializado y
otro cómo se pueden aplicar texturas. En este apartado se verá lo segundo.
Para comenzar una textura es un método para añadir detalle a los gráficos o modelos 3D. La
técnica más común es aplicar una imagen ya existente sobre una malla de vértices. Los
resultados pueden ser tan buenos como lo sea la textura, pudiendo conseguir que un modelo
3D tenga un aspecto más real. También significa una mejora del rendimiento ya que, por
ejemplo, en lugar de modelar una piedra que podría tener una gran cantidad de vértices,
debido a su superficie irregular, que harían enlentecer el juego, podría simularse el aspecto de
piedra aplicando una textura a un modelo mucho más simple consiguiente la apariencia
deseada sin apenas costes de rendimiento. En el siguiente ejemplo se pintará un cubo girando
con una textura de caja de madera, ver figura 10.
// Texturas
#include <PA9.h>
float rtri;
float rquad;
int DrawGLScene();
int main(void)
{
PA_Init();
PA_InitVBL();
PA_Init3D();
glEnable(GL_ANTIALIAS);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
while (1)
{
glPushMatrix();
DrawGLScene();
glPopMatrix(1);
glFlush(0);
PA_WaitForVBL();
}
return 0;
}
int DrawGLScene()
{
// Inicializa la matriz de texturas
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f,0.0f,-4.0f);
glRotatef(rtri,0.6f,1.0f,0.8f);
glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE);
glBegin(GL_QUADS);
// Cara frontal
glNormal3f( 0.0f, 0.0f, 1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// Cara trasera
glNormal3f( 0.0f, 0.0f,-1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// Cara superior
glNormal3f( 0.0f, 1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// Cara inferior
glNormal3f( 0.0f,-1.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
rtri+=0.9f;
rquad-=0.75f;
return TRUE;
}
Loading 'textura.bmp'...
Image size: 128 x 128...
Converting...
Done!
Esto indica el nombre de la textura a convertir, su tamaño en ancho x alto y finalmente que la
conversión se ha realizado con éxito. Finalmente copiamos el archivo generado,
Crate.bin, en la subcarpeta data de nuestra aplicación.
expresa con dos componentes ya que la textura está en dos dimensiones. Este proceso es
conceptualmente similar a envolver un objeto con un papel estableciendo puntos en común,
de este modo, se hace corresponder una coordenada del papel (2D) con una coordenada del
objeto (3D).
3.3.6 Iluminación
Antes se ha mencionado un poco el tema de la iluminación, pero en este capítulo se estudia
las posibilidades que ofrece la adaptación de openGL para la videoconsola NDS y como
iluminar una escena.
En openGL existen varios tipos de luces con las cuales se pueden simular las distintas
iluminaciones que pueden observarse en el mundo real. Algunas de estas son iluminación
global cuya luz es homogénea y cubre la escena por igual, luz de foco con un cuerpo cónico
que tiene un diámetro que aumenta con la distancia, luz puntual que se extiende en todas
direcciones a partir de su posición… Sin embargo, la versión de openGL que estamos usando
sólo ofrece un tipo de luz llamada paralela. Esta tiene como característica que su posición se
sitúa en una paralela de la escena en el infinito. Para crear una luz hay que establecer un
número que la identifique, un color RGB de iluminación y una coordenada (x,y,z). La
posición se expresa normalizada, entre -1 y +1, e indica en cual paralela del infinito se sitúa el
foco. Por ejemplo, si la coordenada es (0,+1,0) la luz estará situada por delante de la escena,
se verá que ilumina de frente; si la luz está en (0,-1,0) estará por detrás. Si la posición x es +1
iluminará por la izquierda y -1 lo hará por la derecha. Y así con el resto de coordenadas.
Las macros se van a usar para definir los estados de movimiento de la luz, respecto al
cuadrado imaginario descrito antes. La posición está contenida en las variables xCam e yCam,
el tramo actual en tramo y el desplazamiento en cada instante por inc.
while (1)
{
PA_ClearTextBg(1);
glPushMatrix();
DrawGLScene();
glPopMatrix(1);
glFlush(0);
PA_WaitForVBL();
case TRAMO_DERECHO:
if(yCam < 1.0f)
yCam += inc;
else{
tramo = TRAMO_INFERIOR;
xCam -= inc;
}
break;
case TRAMO_INFERIOR:
if(xCam > -1.0f)
xCam -= inc;
else{
tramo = TRAMO_IZQUIERDO;
yCam -= inc;
}
break;
case TRAMO_IZQUIERDO:
if(yCam > -1.0f)
yCam -= inc;
else{
tramo = TRAMO_SUPERIOR;
xCam += inc;
}
break;
}
}
Por último, dentro de la función DrawGLScene se crea la luz con glLight, se activa o no
en función de si está pulsado el botón Start y finalmente se dibuja el cubo. Es importante
darse cuenta de que es necesario que se definan las normales del objeto para que se pueda
iluminar, sino no se vería nada en la pantalla. Igual de necesario es definir las propiedades de
iluminación con glMaterialf.
int DrawGLScene()
{
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f,0.0f,-4.0f);
glRotatef(45.0f,1.0f,1.0f,0.0f);
glMaterialf(GL_AMBIENT, RGB15(2,2,2));
glMaterialf(GL_DIFFUSE, RGB15(20,0,0));
glMaterialf(GL_SPECULAR, BIT(15) | RGB15(31,0,0));
glMaterialf(GL_EMISSION, RGB15(15,0,0));
glMaterialShinyness();
glColor3f(1.0f,0.0f,0.0f);
glBegin(GL_QUADS);
// Cara frontal
glNormal3f( 0.0f, 0.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
// Cara trasera
glNormal3f( 0.0f, 0.0f,-1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
// Cara superior
glNormal3f( 0.0f, 1.0f, 0.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
// Cara inferior
glNormal3f( 0.0f,-1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
// Cara derecha
glNormal3f( 1.0f, 0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
// Cara izquierda
glNormal3f(-1.0f, 0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glEnd();
return TRUE;
}
Pero cuando hablamos de 3D la cosa cambia. Los objetos están inmersos en una escena de
tres dimensiones y lo único que conocemos de la pulsación es una coordenada en dos
dimensiones. Por tanto, se pierde la correspondencia totalmente, entonces ¿cómo se podría
averiguar si un elemento ha sido pulsado en una escena 3D? En este apartado se verá un
“truco” desarrollado por Gabe Ghearing, colaborador de libnds [4].
Antes de nada definimos el objetivo, queremos pintar una escena donde aparezcan varios
elementos 3D por ejemplo tres cubos, uno rojo otro verde y otro azul, y que cuando el usuario
pulse sobre alguno de ellos el programa sepa sobre cuál lo ha hecho y lo identifique de alguna
manera.
La idea es la siguiente, primero se pinta la escena como ya sabemos hacer, luego lo que hay
que hacer es usar la función de openGL gluPickMatrix, que restringe la zona de
renderizado a un área cuadrada que definamos, pintamos sobre una zona reducida en torno a
la pulsación del puntero. Como la variable GFX_POLYGON_RAM_USAGE almacena la
cantidad de polígonos pintados en pantalla, se compara si ha aumentado el número de
polígonos al pintar sobre ese pequeño área, en tal caso significará que había un objeto sobre la
zona pulsada, si no varía el número de polígonos entonces no había nada en ese área. Bien,
ahora se conoce si había algo allá donde estaba el puntero, falta resolver qué elemento era el
que ahí estaba. Para esto lo que hay que hacer es ir comprobando el número de polígonos en
pantalla por cada elemento que exista. Sin embargo, no tendremos la solución con el primer
elemento que aparezca bajo el puntero ya que puede ser que hubiera varios superpuestos, por
tanto habrá que comprobar además si el elemento que está en esa posición es el más cercano a
la cámara. Esto se puede resolver con ayuda de una librería que trae libnds también creada por
Gabe Ghearing llamada PosTest. Esta contiene un método PosTestWresult() que
habiendo establecido previamente lo que llama una posición de prueba, devuelve la distancia
entre esta y la cámara. De este modo ya sabremos distinguir entre cuál de los objetos se ha
pulsado y cuál está más cerca de la cámara. A continuación se muestra el código del ejemplo:
// Selección de figuras
// Includes
#include <PA9.h>
#include <nds/arm9/postest.h> // Librería usada para detectar posiciones
// Definición de tipos
typedef enum {
NOTHING,
RED,
GREEN,
BLUE
} Clickable;
// Variables globales
uint32 rotateX = 0;
uint32 rotateY = 0;
// Funciones
int DrawGLScene();
int main()
{
PA_Init();
PA_InitVBL();
PA_Init3D();
PA_InitText(1,3);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
while(1)
{
PA_ClearTextBg(1);
if(clicked==RED)
PA_OutputText(1,1,1,"Pulsado cubo Rojo");
else if(clicked==GREEN)
PA_OutputText(1,1,1,"Pulsado cubo Verde");
else if(clicked==BLUE)
PA_OutputText(1,1,1,"Pulsado cubo Azul");
glPushMatrix();
DrawGLScene();
glPopMatrix(1);
glFlush(0);
PA_WaitForVBL();
return 0;
}
void startCheck() {
while(PosTestBusy()); // Espera a que la posición de prueba termine
while(GFX_BUSY); // Espera a que se dibujen todos los
// polígonos del último objeto
PosTest_Asynch(0,0,0); // Crea una posición de prueba
// sobre la actual posición trasladada
polyCount = GFX_POLYGON_RAM_USAGE; // Guarda el número de polígonos
// actuales
}
int DrawGLScene(){
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(30, 256.0 / 192.0, 0.1, 20);
glMatrixMode(GL_MODELVIEW);
if(clicked==RED) {
// Establece poly ID para mostrar línea externa
glPolyFmt(
POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(1));
} else {
// Establece poly ID para no mostrar la línea
// externa (del mismo color que el fondo)
glPolyFmt(
POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(0));
}
drawCube(2.9f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
if(clicked==GREEN) {
glPolyFmt(
POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(1));
} else {
glPolyFmt(
POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(0));
}
drawCube(-3.0f, 1.8f, 2.0f, 0.0f, 1.0f, 0.0f);
if(clicked==BLUE) {
glPolyFmt(
POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(1));
} else {
glPolyFmt(
POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(0));
}
drawCube(0.5f, -2.6f, -4.0f, 0.0f, 0.0f, 1.0f);
}
glPopMatrix(1); // Restaura modelview a donde fue rotada
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// Renderiza sólo lo que esté bajo el cursor
gluPickMatrix(touchX,(191-touchY),4,4,viewport);
// La perspectiva debe ser la misma que la original
gluPerspective(30, 256.0 / 192.0, 0.1, 20);
glMatrixMode(GL_MODELVIEW);
startCheck();
drawCube(2.9f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
endCheck(RED);
startCheck();
drawCube(-3.0f, 1.8f, 2.0f, 0.0f, 1.0f, 0.0f);
endCheck(GREEN);
startCheck();
drawCube(0.5f, -2.6f, -4.0f, 0.0f, 0.0f, 1.0f);
endCheck(BLUE);
}
}
glPopMatrix(1); // Restaura modelview
return TRUE;
}
glColor3f(r,g,b);
glBegin(GL_QUADS);
// Cara frontal
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
// Cara trasera
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
// Cara superior
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
// Cara inferior
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
// Cara derecha
Para identificar al objeto pulsado del resto se hace uso de una propiedad de openGL que
ofrece la posibilidad de dibujar el contorno de un elemento 3D. Esto se activa llamando a
glEnable(GL_OUTLINE). La siguiente línea es glSetOutlineColor que permite
establecer el color de dicha línea con una terna RGB. Además se muestra un mensaje de texto
en la pantalla superior indicando el cubo que se ha seleccionado. Es importante observar que
esto sigue funcionando aunque la escena se gire usando los cursores.
En primer lugar se va a plantear qué tipo de juego se va a realizar y por qué, a continuación se
decide el guión del juego, a partir de este comienza la fase de análisis infiriéndose en esta
cómo se va a estructurar (número de niveles, elementos que lo componen,…).
Otra parte importante del análisis, después de modelar el concepto en los distintos objetos que
se puede descomponer, consiste en analizar los estados en los que se pueden encontrar estos y
cómo transitar de unos a otros. Para ello se usarán técnicas de modelado como UML para la
elaboración de diagramas de casos de uso.
Una vez esté analizada y definida la idea se pasará al siguiente capítulo, fase de diseño, que
abarcará cada elemento por separado de la estructura y lo acercará más todavía a la realidad
del desarrollador aunque sin llegar todavía a la implementación.
4.1 Idea
Para definir lo que se quiere conseguir, se parte de la ventaja de conocer los límites que se
poseen. Podemos aprovechar esta condición para definir un tipo de juego acorde con la
situación.
Se conocen muchos tipos diferentes de juegos, prácticamente puede haber tantas categorías
como ocurrencias distintas se puedan tener. En este paso no tiene tampoco porque ser
necesario justificar cada planteamiento con un motivo técnico/práctico, a parte de la necesidad
de dar algo de libertad a las ideas, simplemente porque en esta etapa tan primaria por mucho
que se pueda imaginar nunca se pueden conocer realmente las consecuencias de las
decisiones.
Por tanto, la idea que va a tratar de desarrollar durante esta segunda parte de la memoria
consiste en un juego de tipo aventura gráfica. La representación del personaje, objetos y
escenario se hará en 3D. Mientras que la interfaz de usuario será implementada en 2D. Usará
la pantalla táctil que proporciona la videoconsola además del resto de botones que esta posee.
El género de aventura gráfica es un concepto bastante genérico donde interviene uno o más
protagonistas, es una aventura donde habrá que explorar, investigar y resolver puzzles. Esta
categoría tiene una fuerte carga en la narración de los hechos sin embargo no será este el
punto fuerte en nuestro caso. La parte atractiva de elegir este género en lugar de otro, es la
posibilidad de crear un pequeño mundo donde intervengan los elementos necesarios para
hacerlo completo. Pudiendo de este modo, cualquier persona que se lo proponga, ampliarlo
sin demasiados inconvenientes dedicándole el peso correspondiente al hilo argumental.
4.2 Guión
La existencia de Internet ha dado la posibilidad de que muchos “pequeños” desarrolladores
muestren con facilidad los programas que van realizando. Muchos son los que hacen sus
prácticas de aprendizaje elaborando juegos ya que ofrece una muy buena posibilidad para
tocar muchos campos que abarcan los desarrollos informáticos. Una de las plataformas más
llamativas que combina de un modo excelente la parte gráfica con la programación es Adobe
Flash [21]. Todo esto viene a que, junto a este tipo de desarrollos llevados a cabo
principalmente por personas que lo hacen por diversión, realizan juegos más o menos simples
que luego cuelgan en la red, conocidos popularmente como minijuegos, han creado un tipo de
aventuras que será la empleada para nuestro desarrollo. Esta categoría es la conocida como
‘Scape Room’. Consiste en juegos de aventura en los que el protagonista aparece, sin saber
muy bien cómo, en una habitación que le resulta desconocida y siente la necesidad
(comprensible por otra parte) de que debe salir de ella. ¿Por qué es tan popular un tipo de
aventura así? Parece lógico pensar que es la mejor manera de realizar una aventura sin tener
que dar muchas explicaciones al usuario. Así que el guión de este juego será algo similar.
La historia completa sería la siguiente: el protagonista, amanece sin saber por qué (puede
suponerse que tampoco le interesa) en una habitación que no le resulta familiar. Allí parece
que no hay nadie, aunque es capaz de reconocer que existe una salida. Se trata de una puerta
que se encuentra cerrada con llave. A su alrededor se encuentran una serie de objetos que
pueden ayudarle a salir de su prisión. ¿Será capaz de conseguirlo? ¿Abrir la puerta será la
solución? ¿Bastará con salir al exterior? ¿Podrá aguantar mucho tiempo sin comer?
el usuario puede interactuar, serán definidos más adelante. El objetivo para completar el
primer escenario es salir de la habitación por la puerta.
Cuando se logre escapar de la habitación, se accede a la siguiente fase. Esta será como un
bosque. Aquí también se pueden encontrar algunos objetos. El objetivo del personaje en este
escenario es diferente. Al haber salido al exterior y encontrarse perdido en el bosque, se
modifican sus prioridades y el protagonista entiende que es mejor estar alimentado que estar
perdido.
4.3.2 Elementos
El elemento más importante y que se muestra a lo largo de la partida es el personaje
protagonista. Este es la representación del usuario en el juego, y como se ha dicho, aparece
desde el comienzo hasta el final. Se enumeran a continuación el resto de objetos que aparecen
en los escenarios.
Es necesario indicar, que de entre todos los objetos que existen, algunos podrá usarlos el
usuario y otros no, y de entre los que se puedan usar, unos servirán para avanzar en la historia
y otros no.
• Primer escenario:
o Mesa: oculta la llave que permite salir del escenario.
o Estatua: mientras esté apoyada encima de la mesa, está no se podrá desplazar.
o Puerta: se abre con la llave. Es por donde debe salir el personaje para continuar en
la siguiente pantalla.
o Fregona: elemento innecesario para la aventura.
o Llave: con esta se podrá abrir la puerta.
• Segundo escenario:
o Árbol: se trata del objeto que debe usar el personaje para obtener el alimento.
o Coco: al obtenerlo el usuario, se acaba la partida.
• Selección: Al usar el puntero se realiza una selección sobre algún elemento del escenario.
Bien puede tratarse de una zona del suelo a la que el actor pueda desplazarse o puede
tratarse de un objeto con el que puede interactuar. Si no es ninguno de los dos casos
anteriores, entonces no se realiza ninguna acción.
• Mover inventario: Las fechas de dirección o cursores, se utilizan para poder seleccionar
alguno de los objetos que haya recogido el protagonista y almacene en su inventario. Esto
sirve para poder usar un objeto del inventario junto con otro objeto que pertenezca a la
escena.
• Cambiar acción: Existen tres acciones que puede realizar el protagonista: ‘Ir a’, ‘Recoger’
y ‘Usar’. Por tanto, se hará uso de tres botones concretos de la videoconsola para marcar
estas acciones.
• Girar cámara: Usando los botones laterales que posee la videoconsola, se gira la cámara
para cambiar la perspectiva de la escena. Con el botón ‘L’ se gira la cámara a la izquierda
y con el botón ‘R’ se cambia hacia la derecha.
Por tanto, en esta fase se abarcan los siguientes puntos: diseño del personaje y objetos, diseño
de la interfaz de usuario y estudio de las clases que componen el proyecto.
El diseño de los elementos gráficos corresponde al equipo de arte del proyecto y pueden ser
tan complicados como el director lo requiera. Para este caso concreto se necesitan modelos en
3D. Se ha definido ya la lista de objetos que forman parte del juego. A excepción del
personaje protagonista, el resto de objetos son simples: mesa, llave, puerta, árbol… y sobre
estos no se presentan diseños previos. Sin embargo, para el protagonista se muestra a
continuación el boceto de lo que debe ser el personaje visto de perfil y de frente.
La figura 11 es suficiente para que el diseñador encargado pueda elaborar el modelo en tres
dimensiones como se mostrará en el siguiente capítulo de la implementación. El resto de
componentes incluido el escenario, se crean directamente sobre el programa de modelado.
Es oportuno en este momento dar las gracias a la ilustradora Sandra Arteaga [22] por ceder
sus ilustraciones a este proyecto.
12 - Interfaz de usuario.
Las zonas de color magenta se corresponden con las transparentes, que cuando esté
funcionando la aplicación se verán de color negro ya que no hay nada detrás. Los círculos con
un tono verde claro bordeado por un color verde más oscuro representan los botones que
puede usar el usuario para realizar la acción que viene acompañada en el rótulo más cercano:
• : Gira la cámara a la izquierda.
• : Gira la cámara a la derecha.
El conjunto de cinco filas que se encuentra en el lateral izquierdo muestra un listado con los
objetos que posee el usuario en su inventario. Cuando esté seleccionada la opción ‘Usar’ se
podrá seleccionar alguno de estos elementos del inventario para usarlo junto con algún otro
que se encuentre en el escenario, o bien no seleccionar ninguno del inventario para
simplemente usar un objeto de la escena.
La barra más larga que se encuentra en la zona inferior de la figura 12 muestra el estado
actual en el que el actor se encuentra. Estos pueden ser: “Ir a X”, “Recoger X”, “Usar X”,
“Usar Y con X”. Siendo X objetos que se encuentren en la escena e Y objetos que se hayan
recogido y formen parte del inventario.
13 - Selector acción.
14 - Selector inventario.
15 - Clase UML.
• Figura
El elemento básico es la ‘Figura’. Un objeto se puede componer de varias, por
ejemplo el protagonista tiene cinco figuras: brazo izquierdo, brazo derecho, pierna
izquierda, pierna derecha y torso; de esta manera se puede animar cada figura por
separado. También se debe ver la ‘Figura’ como el objeto que se rellene a partir del
objeto modelado en tres dimensiones, es decir a partir del fichero que se obtenga desde
el programa de diseño 3D. Se observa en el diagrama cómo cada figura puede aportar
un vector de la estructura animación que indica por cada instante o frame su posición,
rotación y escalado.
• Objeto3D
La clase ‘Objeto3D’ es un contenedor de figuras, ofreciendo además la posibilidad
de aportar una caja de colisiones, o bounding box, al conjunto de estas por si fuera
necesario. También posee un vector de materiales del conjunto de figuras que lo
conforman. Se observa como la relación que existe entre ‘Objeto3D’ y ‘Figura’ es
de asociación, pudiendo un ‘Objeto3D’ tener una o muchas ‘Figura’.
• Entidad
Esta clase ya supone un componente de más alto nivel de abstracción. Es el usado en
el desarrollo para representar a los elementos que componen la escena. Por ello tiene
componentes que lo ubican como caraMasCercana, que indica la cara del suelo que
tiene más cercana, o los mensajes que se deben mostrar cuando no se pueda recoger o
usar; o el estado que es una estructura del instante en el que se encuentra en la escena.
La relación entre ‘Entidad’ y ‘Objeto3D’ es de generalización (o especialización
según se mire).
En esta clase se implementa la lógica del juego, se encarga de actualizar los eventos de
teclado y puntero, actualizar las posiciones de los elementos, desencadenar los eventos
correspondientes según el estado en el que se encuentre la aplicación, y finalmente pinta la
escena.
Hasta ahora se ha mostrado la estructura básica de la aplicación. Con esta estructura se puede
partir para la elaboración de cualquier historia diferente. Para este caso en concreto se muestra
en la figura 18 el diagrama de clases utilizado.
A parte de estas clases, se utiliza una librería para simplificar algunas operaciones como por
ejemplo trabajar con vectores o cuaterniones. Esta librería permanece fuera de la estructura de
clases y se incluye en el código según sea necesario.
Capítulo 6. Implementación
En este capítulo se va a utilizar el análisis y diseño llevado a cabo en los apartados anteriores
para realizar la implementación del juego. Se tendrá en cuenta los diagramas elaborados y el
estudio previo de las clases. Sin embargo, también es necesario elaborar los componentes
gráficos que forman parte de la aplicación.
Este capítulo se divide en varias partes. Un primer bloque va a desarrollar cómo implementar
el sistema/esqueleto que hará posible crear esta aventura además de ser el soporte necesario
para que cualquier desarrollador sea capaz de implementar sus aventuras sin tocar en el
núcleo salvo que desee ampliar funcionalidades. El otro bloque implementará la aventura
descrita con todas las clases que es necesario heredar y especializar para dicho fin.
Dentro del primer bloque se distinguen dos grupos que llevarán los dos primeros apartados. El
primero se abarca cómo obtener los componentes gráficos que necesita el juego: imágenes y
objetos 3D. La segunda parte se centra en la implementación del código de la aplicación.
En la fase de diseño se mencionó que existe una parte gráfica en 2D y otra en 3D que se
corresponden con la interfaz de usuario y con los modelos respectivamente.
6.1.1 Sprites
Un sprite es un mapa de bits que representa una imagen que se puede cargar en la aplicación.
Esto es lo que queremos obtener a partir de las imágenes que se vieron en la fase de análisis
para crear la interfaz de usuario. Si recordamos eran tres imágenes, una contiene la interfaz
gráfico de usuario mostrando los botones que debe pulsar el usuario para cambiar la acción,
otra imagen era el selector para marcar la acción seleccionada por el usuario, y la última se
trataba del selector de objetos del inventario. Estas tres imágenes se almacenan en tres
ficheros diferentes: menu.png, selector.png y selectorItem.png. Como ya se
comentó en el capítulo 3 donde se explicaba cómo cargar imágenes haciendo uso de la librería
PAlib, es necesario utilizar la herramienta PAGfx para convertir las imágenes png en sprites.
Esta operación viene explicada en el apéndice de cómo utilizar esta herramienta. El resultado
de dicha operación son ocho ficheros: all_gfx.h, all_gfx.c, menu.c, menu.pal.c,
selector.c, selector.pal.c, selectorItem.c y selectorItem.pal.c. Los
dos primeros contienen alias para poder incluir los gráficos en el proyecto de una manera
sencilla. El resto de ficheros representan el mapa de bits y la paleta asociada de las imágenes
convertidas. El conjunto de estos ficheros se copiará en la carpeta /gfx de la carpeta
source de nuestra aplicación.
Lo primero consiste en obtener el modelo 3D. Existe una gran variedad de herramientas de
modelado en 3D muchas de ellas muy sofisticadas. Para nuestro trabajo no hace falta que
ofrezca grandes posibilidades ya que estas suelen estar enfocadas en la producción de
imágenes o vídeos renderizados en 3D. No es nuestro caso, el objetivo es obtener una malla
texturizada con pocos polígonos para poder representarlos en tiempo real. Por estos motivos
se ha usado una herramienta llamada wings3d [24]. Se trata de una aplicación de modelado
3D gratuita, de libre distribución, y de código abierto. Es sencilla, ocupa poco, y permite crear
modelos en 3D, aplicarles texturas y poder exportarlos a una gran variedad de formatos
diferentes. Quizás una desventaja sea que de momento no soporta animar los modelos, por lo
que se deberá usar otra herramienta para ello. Una vez elegida la herramienta, se procede a la
creación del modelo. Partimos de una imagen que se vio en el capítulo anterior que
representaba la imagen del protagonista visto desde perfil y de frente. El objetivo de esta guía
no es aprender a crear modelos en 3D pero a continuación se dejan algunos pasos que se han
seguido para este proceso. Primero se colocó la imagen de perfil y de frente como dos planos
de pie en el suelo formando un ángulo de 90º como se puede observar en la imagen 19. De
esta manera es fácil ir creando primitivas 3D como cubos, esferas, cilindros… e ir dándole la
forma que se necesita. Es importante tener en cuenta que este modelo va a animarse, esto
implica que no puede estar compuesto por una sola pieza, debe estar compuesto por diferentes
partes que se puedan animar individualmente. En nuestro caso serán cinco piezas: pierna
derecha, pierna izquierda, brazo derecho, brazo izquierdo y torso.
Una vez obtenido el modelo (ver figura 20), el siguiente paso es dotarlo de textura. Esto se
consigue mediante el proceso conocido como mapeo UV que consiste en desenvolver la malla
de la que está compuesto el modelo en un plano de dos dimensiones. En la figura 21 se puede
apreciar como se ha distribuido la malla sobre la imagen que se corresponde con la textura, y
a su lado el resultado final del modelo texturizado.
21 - Textura usada.
22 - Modelo texturizado.
Ya hemos conseguido un modelo 3D (ver figura 22), el siguiente paso es animarlo. Como ya
se ha comentado, el programa wings3D no da soporte para animación, así que se debe usar un
programa diferente. En principio sirve cualquiera que soporte las animaciones básicas de
traslación, rotación y escalado. Elegimos por ejemplo uno de los programas más populares,
Autodesk 3ds Max [25] que se puede obtener una versión de prueba de 30 días. Este es uno de
los más completos y más usados, aunque lo hemos elegido como podría haber sido cualquier
otro que permitiera animar. Además nos sirve para el paso posterior que consiste exportar el
modelo animado al formato DirectX. Para ello es necesario instalar un plugin desarrollado por
la empresa PandaSoft que se puede descargar en este enlace [26].
La animación de un modelo también puede ser algo muy elaborado. Existen diferentes
técnicas que dan distintos resultados según sea la finalidad. Por ejemplo, si se quiere obtener
un movimiento realista, lo mejor es adjuntar al modelo 3D un esqueleto donde cada hueso se
vincule con una sección de la malla. Con esto se consigue que al animar algún hueso, los que
se encuentren conectados a este se desplacen arrastrados por él. Para el caso que nos ocupa no
es necesario invertir tanto sacrificio y podemos usar una de las técnicas de animación más
tradicionales. Consiste en que teniendo una línea de tiempo en la que transcurre la animación,
se seleccionan varios fotogramas clave donde disponemos del modelo en la posición que nos
interese. El programa de modelado rellenará los fotogramas intermedios realizando una
interpolación de la malla.
En nuestro caso la animación que nos interesa es que nuestro protagonista camine. Para
simplificarlo, lo dejaremos en un movimiento contrario de piernas. La secuencia se muestra
en las figuras 23 a 26.
23 - Frame 0. 24 - Frame 5.
Ya tenemos el personaje animado. Ahora viene la parte de obtener un formato que se pueda
interpretar y convertir en una estructura reconocible por nuestro programa. Estudiando los
distingos formatos que existen, hemos elegido uno de los formatos de entre todas las
posibilidades que cumplían nuestras necesidades. Esto es que sea sencillo leer la estructura
que forma el modelo, almacene animaciones y sea sencillo de generar. Se trata del formato
DirectX [27]. Es un formato bastante extendid, propietario de Microsoft y empleado en
Microsoft Windows.
para conocer que parte de la imagen pintar sobre la figura. Cada cara de la malla puede ser
asociada a un material diferente.
Bloque Contiene
Frame FrameTransformMatrix
Frame
Mesh
Mesh MeshNormals
MeshTextureCoords
MeshMaterialList
SkinMeshHeader
SkinWeights
MeshMaterialList Material
Material TextureFileName
AnimationSet Animation
Animation AnimationKey
Tabla 1 - - Estructura fichero X
27 - Modelo 3D de prueba.
Se trata de un modelo 3D compuesto por dos componentes, uno localizado arriba de color
amarillo con nombre ‘cabeza’ y otro de color verde turquesa nombrado ‘cuerpo’ a las que se
le ha aplicado una textura plana a cada uno. Además el modelo está animado de manera que el
componente amarillo da un giro completo en 40 frames. Iremos viendo paso a paso cada
sección del fichero DirectX obtenido.
• Plantillas
template EffectInstance {
<e331f7e4-0559-4cc2-8e99-1cec1657928f>
STRING EffectFilename;
[...]
}
template EffectParamFloats {
<3014b9a0-62f5-478c-9b86-e4ac9f4e418b>
STRING ParamName;
DWORD nFloats;
array FLOAT Floats[nFloats];
}
template EffectParamString {
<1dbc4c88-94c1-46ee-9076-2c28818c9481>
STRING ParamName;
STRING Value;
}
template EffectParamDWord {
<e13963bc-ae51-4c5d-b00f-cfa3a9d97ce5>
STRING ParamName;
DWORD Value;
}
Aquí se muestran las plantillas usadas para este documento, que aparecen al comienzo del
fichero. Estas son: plantilla FVFData para especificar los datos de la malla restando la
posición, plantilla EffectInstance para definir plantillas de efectos, plantilla
EffectParamFloats para definir efectos con números en coma flotante, plantilla
EffectParamString para definir efectos con cadenas de texto y plantilla
EffectParamDWord para definir efectos con el tipo DWord.
• Materiales
La etiqueta Material define un material color básico que puede ser aplicado bien a una malla
completa o un conjunto individual de caras. Cada grupo Material define los siguientes
aspectos: color de la cara en formato RGBA, exponente del color especular como un Float,
color del material especular y color del material emisivo, ambos últimos en formato RGB.
Material cabeza_auv {
1.000000;1.000000;1.000000;1.000000;;
0.000000;
0.000000;0.000000;0.000000;;
0.000000;0.000000;0.000000;;
TextureFilename {
"cabeza_a.bmp";
}
}
Material rotor_auv {
1.000000;1.000000;1.000000;1.000000;;
0.000000;
0.000000;0.000000;0.000000;;
0.000000;0.000000;0.000000;;
TextureFilename {
"rotor_au.bmp";
}
}
• Frame cabeza
Es una plantilla abierta que puede contener cualquier objeto en una subplantilla Mesh junto a
una transformación en una plantilla FrameTransformMatrix.
Frame cabeza {
FrameTransformMatrix {
1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000
,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.00000
0,0.000000,1.000000;;
}
Mesh {
329;
0.300000;-0.007822;1.121040;,
0.212132;-0.219954;1.121040;,
0.000000;-0.307822;1.121040;,
…continúa…
0.000000;-0.181689;1.645504;,
0.000000;-0.181689;1.645504;,
0.000000;-0.181689;1.645504;;
110;
3;231,252,226;,
3;239,249,234;,
3;248,254,245;,
…continúa…
3;244,252,240;,
3;36,40,35;,
3;218,251,246;;
MeshNormals {
176;
-0.106596;-0.257344;-0.960422;,
-0.110311;0.000000;-0.993897;,
-0.106596;-0.257344;-0.960422;,
…continúa…
0.257344;0.106596;-0.960422;,
0.257344;0.106596;-0.960422;,
0.257344;0.106596;-0.960422;;
110;
3;0,1,2;,
3;3,4,5;,
3;6,7,6;,
…continúa…
3;171,1,171;,
3;172,172,172;,
3;173,174,175;;
}
MeshMaterialList {
1;
110;
0,
0,
0,
…continúa…
0,
0,
0;
{ cabeza_auv }
}
MeshTextureCoords {
329;
0.285878;0.113956;,
0.401557;0.113956;,
0.000000;0.113956;,
…continúa…
0.600760;0.876275;,
0.653137;0.113956;,
0.956158;1.000000;;
}
}
}
La primera línea que encontramos indica el nombre de la figura que representa este frame, en
este caso se trata de la figura ‘cabeza’. Lo siguiente es una plantilla denominada
FrameTransformMatrix que indica la transformación local de representación aplicada al
frame aplicando una matriz 4x4. Después aparece la plantilla Mesh que representa la
estructura que forma la malla, a su vez contiene varias plantillas: MeshNormals,
MeshMaterialList y MeshTextureCoords. El frame Mesh comienza indicando el
número de vértices de la estructura ‘cabeza’, 329, y continúa con una lista de ese tamaño de
coordenadas 3D. Sin embargo, la estructura se organiza en caras, por tanto lo siguiente que
aparece tras la lista de vértices es un número entero que representa el número de caras que
compone la estructura ‘cabeza’, y después de este dato aparece la lista de caras que se forma
de la siguiente manera: <número n de vértices que compone la cara>, <vértice 1 que compone
la cara>, …<vértice i que compone la cara>, …<vértice n que compone la cara>. Como la
lista de vértices es la que precede a esta, no es difícil conocer como está creada la malla del
objeto. Después de esta aparece, MeshNormals que describe las normales, gracias a las
cuales un modelo puede ser iluminado. De un modo similar a como se especifica la malla, lo
primero que se observa es un número entero que identifica la cantidad de coordenadas que
representan normales y tras esto aparece otro valor numérico entero que coincide con el
número de caras de manera que cada una de las líneas siguientes indica tres normales del
listado inmediatamente anterior, que se corresponde cada una de ellas con la normal que tiene
asociada cada uno de los tres puntos que forma la cara. Por ejemplo, en nuestro caso la
primera cara se forma con las coordenadas 231, 252 y 226; que se corresponde con las
normales representadas por los índices 0, 1 y 2. El siguiente bloque es
MeshMaterialList para hallar la correspondencia entre caras y materiales. El primer
número representa el número de materiales asociados, el siguiente es el número de caras (que
coincide con las otras ocasiones en las que aparece este valor), después viene el listado de
correspondencia entre cada cara y el material. Por último se muestra el nombre de los
materiales. En nuestro caso la malla cabeza sólo tiene asociado un material (aparece un 1), el
número de caras es 110, en el listado de 110 caras, todas están asociadas al material 0, que
como se observa después se trata de cabeza_auv. El último bloque se utiliza para asociar la
textura a la malla. La idea es que cada coordenada del modelo en tres dimensiones, se
corresponde con una coordenada de la textura en dos dimensiones. El primer número de este
Animation Anim-cabeza {
{ cabeza }
AnimationKey {
0;
41;
0;4;1.000000,0.000000,0.000000,0.000000;;,
1;4;1.000000,0.000000,0.000000,0.000000;;,
2;4;1.000000,0.000000,0.000000,0.000000;;,
…continúa…
38;4;1.000000,0.000000,0.000000,0.000000;;,
39;4;1.000000,0.000000,0.000000,0.000000;;,
40;4;1.000000,0.000000,0.000000,0.000000;;;
}
AnimationKey {
1;
41;
0;3;1.000000,1.000000,1.000000;;,
1;3;1.000000,1.000000,1.000000;;,
2;3;1.000000,1.000000,1.000000;;,
…continúa…
38;3;1.000000,1.000000,1.000000;;,
39;3;1.000000,1.000000,1.000000;;,
40;3;1.000000,1.000000,1.000000;;;
}
AnimationKey {
2;
41;
0;3;0.000000,0.000000,0.000000;;,
1;3;0.000000,0.000000,0.000000;;,
2;3;0.000000,0.000000,0.000000;;,
…continúa…
38;3;0.000000,0.000000,0.000000;;,
39;3;0.000000,0.000000,0.000000;;,
40;3;0.000000,0.000000,0.000000;;;
}
aplicación con C++, que posee una interfaz gráfica sencilla. La idea es generar a partir del
archivo .X en formato texto un archivo binario de datos con una estructura que podamos
utilizar.
• Estructuras de datos
o coordFloat
Representa una coordenada y se compone por los tres componentes x, y, y z de la malla y por
las dos componentes u y v de la textura.
typedef struct{
float x, y, z;
float u, v;
} coordFloat;
o coordCaras
Representa una cara, que siendo fiel a la estructura del fichero X, está formado por tres
índices que se corresponde cada uno con una estructura coordFloat, y además el índice que
indica el material que se asocia con esa cara.
typedef struct{
int pto1, pto2, pto3;
int material;
} coordCaras;
o material
Cada material que compone el modelo 3D contiene un nombre identificativo, un nombre de
fichero, un ancho, un alto, y una metainformación necesaria para conocer el número de
repeticiones que tiene en el modelo.
typedef struct{
char nombre[MAXCAD];
char fichero[MAXCAD];
int ancho, alto;
int veces;
} material;
o animacion
Cada frame de animación de un modelo se identifica por una rotación compuesta por cuatro
componentes (esto se debe a que no son las típicas coordenadas en (x, y,z) sino se trata de
cuaterniones que se explicarán más adelante) y un cantidad de escalado y de posición en
(x,y,z).
typedef struct{
float rot[4], sca[3], pos[3];
} animacion;
o figura
En este caso se ha utilizado una clase para representar a cada figura. Recordamos que un
modelo puede estar formado por varias figuras. En el caso que mostramos arriba, el modelo
está formado por la figura ‘cabeza’ y la figura ‘cuerpo’. La figura posee un nombre
identificativo, un número de frames, un número de caras, un número de puntos, un número de
materiales, un array de nCaras donde cada posición contiene el índice del material que se
asocia con cada cara, un array de nMateriales donde cada posición se corresponde con las
veces que se repite un material, un array que se corresponde con el índice de vértices y un
array de estructura animacion.
class figura{
public:
char nombre[MAXCAD];
int nFrames;
int nCaras;
int nPuntos;
int nMateriales;
int *numeroMat;
int *vecesMat;
float *v;
animacion *a;
};
o modelo
La clase modelo es la que representa al objeto 3D en su totalidad. Se compone de un nombre,
un número de figuras, un vector de figuras, un número de materiales y un vector de materiales
class modelo{
public:
char nombre[MAXCAD];
int nFiguras;
figura *f;
int nMateriales;
material *mat;
};
• Implementación
Esta aplicación de escritorio presenta la siguiente interfaz mostrada en la figura 28.
Como se puede observar, la única información de entrada que necesita es la ruta del fichero X
y la ruta destino del fichero binario. Una vez introducidas se pulsa sobre el botón ‘Convertir’
y se muestra en ‘Salida’ si la conversión se ha realizado correctamente. La lógica de la
aplicación se encuentra en la función:
Esta recibe la ruta del fichero a leer X, la ruta de destino, el nombre del fichero y el mensaje
que se debe mostrar finalmente en la caja de texto ‘Salida’. Esta función se divide en dos
partes bien diferenciadas: lectura del fichero X y creación del fichero binario.
El bloque ‘Material’ almacena una nueva estructura material de la que obtiene su nombre y el
nombre de la textura; despreciando la información del color y resto de valores previamente
explicados. Con el nombre del archivo lo que también hace es llamar a la función
obtenerDimensionesBMP que carga el material de la estructura de ficheros y a través de
una librería obtiene el valor de ancho y alto de esta para incorporarlo a la información de la
estructura de datos.
El tercer tipo de bloque que se puede encontrar, comienza con la cadena de texto ‘Frame’.
Este es más completo que los otros dos ya que aporta más información. Lo primero es obtener
el nombre de la figura. Tras esto se busca la cadena ‘Mesh’ que será la que describa la
formación de la malla del modelo. Como ya se ha estudiado, de aquí se obtiene el listado de
vértices y caras que forman la figura. Estos se guardan en las estructuras coordFloat y
coordCaras. Después se guarda la lista de materiales que interviene en el modelo almacenado
dentro del subbloque ‘MeshMaterialList’. El siguiente bloque que aparece se encuentra
precedido por la cadena ‘MeshTextureCoords’. Aquí se guardan las componentes u y v
de la estructura coordFloat.
/************************************
char nomfich[MAXCAD];
sprintf(nomfich,"%s\\%s.bin",rutaDestino,mod.nombre);
fos.close();
/************************************
* Fin fichero .bin
************************************/
Tras estos pasos hemos conseguido obtener un fichero de datos que podremos cargar en
nuestra aplicación para poder mostrar una figura 3D.
6.2.1 Librerías
Se han creado tres grupos de herramientas que se usan como librerías para facilitar la
estructura del código. Por un lado se encuentra Vectores.h que implementa clases que
simplifican el trabajo con vectores, después unas herramientas en la librería
glQuaternion.h que permiten usar los mencionados pero no explicados cuaterniones, y
por último lib.h que contiene algunas estructuras de datos y funciones varias.
6.2.1.1 Vectores.h
En este fichero se crean dos clases: vector2 y vector3. Ambas definen a vectores con
tipo de dato float, la primera de dos componentes y la segunda de tres. La ventaja que
aporta esta implementación de vectores, es que se han definido una serie de operadores para
simplificar la escritura del código al trabajar con ellos.
• vector2
A continuación se muestra la declaración de esta clase que se encuentra en el fichero
Vectores.h:
/**
* Clase vector2
* Vector de dos componentes (x,y) de tipo float. Contiene funciones
* y operadores que facilita trabajar con esta clase
* @author Fernando García Bernal
*/
class vector2
{
public:
// Miembros
float x, y;
public:
// Constructores
public:
// Operadores
/** Asignación */
vector2 &operator = (const vector2 &v);
public:
// Métodos
• vector3
Las posibilidades que ofrece la clase vector3 son similares a las de la clase vector2. A
continuación se muestra su definición.
/**
* Clase vector3
* Vector de tres componentes (x,y,z) de tipo float. Contiene
* funciones y operadores que facilita trabajar con esta clase
* @author Fernando García Bernal
*/
class vector3
{
public:
// Miembros
float x, y, z;
public:
// Constructores
public:
// Operadores
/** Asignación */
vector3 &operator = (const vector3 &v);
public:
// Métodos
6.2.1.2 glQuaternion.h
Esta librería ha sido implementada para poder trabajar con cuaterniones, pero primero… ¿qué
es un cuaternión? Un cuaternión describe un vector de cuatro dimensiones que generaliza a
los números complejos. Hamilto los inventó en el sigo XIX para las rotaciones, antes de que
se inventaran las matrices de rotaciones. Las operaciones con cuaterniones son
computacionalmente más eficaces que las multiplicaciones de matrices 4x4, y se utilizan para
transformaciones y rotaciones. Además, la ventaja está en la animación, ya que interpolar un
cuaternión resulta sencillo, al contrario que interpolar una matriz de rotación que al ser
ortogonal debe garantizar que el interpolante sigua siendo una matriz de rotación ortogonal.
Los cuaterniones agregan un cuarto elemento a los valores (x,y,z) que definen un vector,
generando vectores arbitrarios de cuatro dimensiones. Sin embargo, las fórmulas siguientes
muestran cómo cada elemento de un cuaternión se refiere a una rotación de un ángulo
alrededor de un eje. Por el motivo de la simplificación del coste de computación que supone
trabajar con estos elementos, se opta por su utilización en la mayoría de los desarrollos de
gráficos 3D para tiempo real. El siguiente código muestra la definición de la clase
glQuaternion:
class glQuaternion
{
public:
/**
* Multiplicación de cuaterniones:
* Esta es la operacion mas comun en cuaternions.
* Estas operaciones pueden ser consultadas en la Url inidicada
* en la cabecera anterior.
* @param q cuaternion por el que se quiere multiplicar
*/
glQuaternion operator *(glQuaternion q);
/**
* A partir de un cuaternion generamos una matriz que podemos usar
* para mandar la rotacion en forma de matriz a openGL con el comando
* glMultMatrix()
* @param *pMatrix Puntero a la zona de memoria que se ha reservado
* para llenarse con el resultado de la multiplicacion. La matriz ha
* de ser float[16], si no dara problemas
*/
void CreateMatrix(float *pMatrix);
/**
* Crear un cuaternion a partir de un angulo y tres ejes. Los
* parametros estan dispuestos casi de la misma manera que el comando
* de openGL glRotatef( el angulo va delante en la rotacion de
* openGL). El significado de esta funcion es crear una rotacion.
* Como cada cuaternion representa una rotacion, despues sólo tenemos
* que multiplicar los cuaterniones para crear una rotacion a medida.
* @param x representa el eje x de la rotacion
* @param y representa el eje y de la rotacion
* @param z representa el eje z de la rotacion
/**
* Suma dos cuaternions.
* @param q cuaternion suma
* @return cuaternion que representa la suma del cuaternion q con el
* representado por la clase
*/
glQuaternion operator +(glQuaternion & q);
/**
* Resta dos cuaternions.
* @param q cuaternion suma
* @return cuaternion que representa la resta del cuaternion q con el
* representado por la clase
*/
glQuaternion operator -(glQuaternion & q);
/**
* Contructor de la clase. Resetea los valores internos de la clase
*/
glQuaternion();
/**
* Contructor de la clase
*/
glQuaternion(float x, float y, float z, float w);
virtual ~glQuaternion();
public:
float m_w;
float m_z;
float m_y;
float m_x;
};
• Constantes
Estas constantes se usan para indicar longitudes máximas:
• Tipos de datos
Algunas de estas estructuras ya se han comentado y otras se usarán más adelante:
• Funciones
Dos funciones genéricas: una para cargar texturas y otra que devuelve el signo (-1, 0 ó 1) de
un valor decimal. Después se encuentran tres funciones usadas en el fichero principal para
inicializar aspectos del juego.
/**
* Carga una textura para después poder vincularla y pintarla
* usando las primitivas correspondientes de openGL
* @param texture puntero que se corresponde con la textura en memoria
* @param identificador al que se asocia la textura
* @param tamaño en ancho
* @param tamaño en alto
*/
void LoadTex(u8* texture, int id, int sizeX, int sizeY);
/**
* Devuelve el signo del float n pasado como parámetro
* @param n valor decimal del que se quiere conocer su signo
* @return devuelve 0 si n es 0.0f, -1 si es menor que 0.0f o 1 si es
* mayor de 0.0f
*/
int signo(float n);
/**
* Función utilizada para inicializar y cargar
* las imágenes de la interfaz de usuario
*/
void inicializarMenu();
/**
* Inicializa la escena 3D
* con las directivas openGL
*/
void inicializaEscena3D();
/**
* Reinicia la escena para que
* pueda cargarse un escenario
*/
void reiniciarEscena();
glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sizeX, sizeY, 0,
TEXGEN_TEXCOORD | GL_TEXTURE_WRAP_S |
GL_TEXTURE_WRAP_T, (u8*)texture);
}
int signo(float n)
{
if(n>0.0f)
return 1;
else if(n<0.0f)
return -1;
else
return 0;
}
void inicializarMenu(){
PA_Init();
PA_InitVBL();
PA_Init3D();
PA_InitText(1,0);
PA_EasyBgLoad(1, 3, menu);
PA_LoadSpritePal(
// Pantalla
1,
// Número de paleta
0,
// Nombre de paleta
(void*)selector_Pal);
void inicializaEscena3D(){
glEnable(GL_ANTIALIAS);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glMaterialf(GL_AMBIENT, RGB15(31,31,31));
glMaterialf(GL_DIFFUSE, RGB15(31,31,31));
glMaterialf(GL_SPECULAR, BIT(15) | RGB15(31,31,31));
glMaterialf(GL_EMISSION, RGB15(31,31,31));
glMaterialShinyness();
gluLookAt(
// Posición de la cámara
0.0, 0.0, -1.0,
// Look at
0.0, 0.0, 0.0,
// Vector alzado
0.0, 1.0, 0.0);
void reiniciarEscena()
{
PA_ClearTextBg(1);
/**
* Conjunto de vértices y caras que forma una figura.
* Una o varias figuras forman un objeto 3d.
* @author Fernando García Bernal
*/
class Figura{
public:
/**
* número de materiales ó texturas que compone la figura
*/
int nMateriales;
/** vector con los números de veces que se repite cada material */
int *vecesMat;
Figura();
~Figura();
};
/**
* La clase Objeto3D es un contenedor de figuras. También posee un
* vector de materiales del conjunto de figuras que lo conforman.
*/
class Objeto3D{
public:
/**
* Constructor con inicialización
* @param _nombre Nombre del archivo binario donde se encuentra
* el objeto 3D
*/
Objeto3D(const void *_nombre){
Objeto3D();
cargarObjeto3D(_nombre);
}
~Objeto3D(){};
/**
* carga el objeto3D
* @param _nombre Nombre del archivo binario donde se encuentra
* el objeto 3D
*/
void cargarObjeto3D(const void *_nombre);
};
El método más importante de esta clase es el que implementa la lógica necesaria para cargar
el fichero binario en un objeto que pueda ser interpretado en la aplicación.
opción de usarse o combinarse con otros. Todas estas opciones comentadas son modificables
al antojo del diseñador del juego accediendo a las herramientas de este objeto Entidad.
Aquí se muestra la definición de la clase:
/**
* La clase Entidad representa un objeto que se puede encontrar
* en el Escenario. Esta Entidad tiene una posición, un frame,
* puede ser iluminada y puede recogerse o usarse según el
* estado de la partida
*/
class Entidad : public Objeto3D
{
public:
Entidad(){};
/**
* Constructor que recibe el nombre de la figura y la textura
* que le corresponde. El tamaño de la textura se toma por
* defecto como 128
* @param _nombre nombre del archivo binario donde se encuentra
* la Entidad
* @param _textura nombre de la textura
*/
Entidad(const void * _nombre, const void * _textura);
/**
* Constructor que recibe el nombre de la figura, la textura que
* le corresponde y el tamaño de esta
virtual ~Entidad(){};
/**
* Devuelve la posición de la cara más cercana del
* escenario para poder ubicar donde se encuentra la entidad
* @return cara más cercana del escenario
*/
int situacionEntidad();
private:
/**
* inicializa las texturas
* @param _textura nombre de la textura que se asocia a la
* Entidad
*/
un Personaje ya que sólo puede existir una clase de este tipo. Definición de la clase
Personaje:
/**
* Clase Personaje. Sólo puede haber una por Escenario ya que
* sólo puede manejarse un protagonista. El resto de elementos
* que componen la escena son de tipo Entidad
*/
class Personaje : public Entidad
{
public:
/**
* Constructor que recibe el nombre de la figura y la textura
* que le corresponde. El tamaño de la textura se toma por
* defecto como 128
* @param _nombre nombre del archivo binario donde se encuentra
* la Entidad
* @param _textura nombre de la textura
*/
Personaje (const void * _nombre, const void * _textura) :
Entidad(_nombre, _textura){};
Personaje (){};
~Personaje (){};
};
/**
* Clase que representa un escenario completo.
* Desde esta clase se coordinan todos los elementos que componen la
* escena: un suelo, un personaje y cero o más elementos.
* Primero se actualizan los eventos hardware, después se realizan las
* acciones oportunas para que finalmente se actualice la escena.
*/
class Escenario
{
public:
/**
* Entidad que contiene al personaje que el usuario
* manejará
*/
Personaje Protagonista;
/**
* Vector de entidades seleccionables que conforman la
* totalidad del escenario junto con el suelo, el personaje y
* los elementos no seleccionables
*/
vector<Entidad*> elementosSeleccionables;
/**
* Vector de entidades no seleccionables que conforman la
* totalidad del escenario junto con el suelo, el personaje y
* los elemento seleccionables
*/
vector<Entidad> elementosNoSeleccionables;
/**
* Vector de entidades que corresponde con el inventario
* recogido por el protagonista
*/
vector<Entidad> inventario;
/**
* Determinado el estado actual del juego. Pueden ser:
* ESTADO_PARADO, ESTADO_DESPLAZAMIENTO
* ESTADO_ANIMACION
*/
int estado;
/**
* Acción del menú seleccionada. Pueden ser:
* ACCION_IR
* ACCION_RECOGER
* ACCION_USAR
*/
int accion;
/**
* Mensaje de información a mostrar en la pantalla
* superior
*/
char mensajePantalla[30];
/**
/**
* indica el fin de la animación
* @see entidadEnAnimacion
*/
int finAnimacion;
/**
* vector de cadenas que se utiliza para que cada
* Escenario ponga los textos que debe mostrar al
* usuario
* @see mostrarTextoPantalla
*/
vector<string> mensajes;
private:
/**
* Subconjunto de las caras que componen el suelo, de las cuales
* el personaje se puede desplazar por encima suya. Por
* convenio, son estas las caras que tienen sus tres
* vértices a la misma altura y están por encima
* del plano Y=0
*/
vector<int> carasDesplazables;
/**
* Array de arrays que por cada cara desplazable contiene una
* lista de las caras desplazables que son contiguas a esta.
* cont[caraDesplazable_i] = lista de caras contiguas
*/
vector<int> *cont;
/**
* Variable global establecida por la función zonaPulsada.
* Contiene la cara de suelo o elemento pulsado.
* @see zonaPulsada()
*/
int clicked;
/**
* Variable global usada por la función zonaPulsada.
* Distancia más cercana a la cámara.
* @see zonaPulsada()
*/
int closeW;
/**
* Variable global usada por la función zonaPulsada.
* Guarda el número de polígonos dibujados
* @see zonaPulsada()
*/
int polyCount;
/**
* Variables globales usadas por la función zonaPulsada.
* Guarda la posición pulsada.
* @see zonaPulsada()
*/
int touchX, touchY;
/**
* Variables globales usadas por la función zonaPulsada.
* usado por gluPickMatrix()
* @see zonaPulsada()
*/
int *viewport;
/**
* Camino a recorrer por el personaje durante el desplazamiento
*/
vector<int> camino;
/**
* Cámara: posición(x,y,z), lookat(x,y,z), vector alzado(x,y,z)
*/
float cam[3][3];
public:
/**
* Constructor
* @param _suelo datos binarios que contiene los datos de la
* Entidad Suelo
* @param _textura datos binarios que contiene la textura del
* suelo
* @param _tamanoText tamaño de la textura
* @param _inc grados de inclinación del suelo
* @param _dist distancia de representación en el eje Z
* @param _alt altura de representación en el eje Y
*/
Escenario(const void * _suelo, const void * _textura, int
_tamanoText, float _inc, float _dist, float _alt);
virtual ~Escenario(){};
/**
* Vincula una Entidad Personaje al escenario que tomará el
* papel de personaje de la escena,
* controlado por el usuario
* @param _personaje Entidad personaje
* @param x Posición x donde aparece el Personaje
* @param y Posición y donde aparece el Personaje
* @param z Posición z donde aparece el Personaje
*/
void vincularProtagonista(Personaje *_personaje, float x, float
y, float z);
/**
* Vincula una Entidad seleccionable al escenario.
* @param _elementoS Entidad elemento seleccionable
* @param x Posición x donde aparece la Entidad
* @param y Posición y donde aparece la Entidad
* @param z Posición z donde aparece la Entidad
*/
void vincularElementoSeleccionable(Entidad *_elementoS, float x,
float y, float z);
/**
* Desincula una Entidad seleccionable al escenario.
* @param nombre nombre de la Entidad elemento seleccionable
*/
void desvincularElementoSeleccionable(char *nombre);
/**
* Vincula una Entidad no seleccionable al escenario.
* @param _elementoNoS Entidad elemento no seleccionable
* @param x Posición x donde aparece la Entidad
* @param y Posición y donde aparece la Entidad
* @param z Posición z donde aparece la Entidad
*/
void vincularElementoNoSeleccionable(Entidad *_elementoNoS,
float x, float y, float z);
/**
* Según el estado del juego realiza las acciones oportunas.
* Estas son: actualizar los eventos hardware, actualizar la
* acción
* que realiza el Personaje, actualizar el estado de la escena y
* actualizar los textos que se muestran en la pantalla.
* @return devuelve true si el Escenario ha llegado a su fin,
* false si todavía no ha finalizado
*/
bool actualizar();
/**
* Devuelve la cara sobre la que se encuentra el personaje
* @return cara del Suelo donde se encuentra el personaje
*/
int situacionPersonaje();
/**
* Devuelve el camino que hay entre las dos caras del Suelo
* pasadas como parámetros
* @param ini Origen del camino
* @param dest Destino del camino
* @see buscarCamino
* @return vector de caras del Suelo que forman el camino
*/
vector<int> devuelveCamino(int ini, int dest);
/**
* Indica si el protagonista tiene cierta Entidad
* en su inventario
/**
* Con este método se pinta sobre
* la pantalla mensajes en las
* transiciones entre un escenario y otro
* @param inicio primer mensaje a mostrar
* @param fin último mensaje a mostrar
*/
void mostrarTextoPantalla(int inicio, int fin);
private:
/**
* Actualiza los eventos que provocan el hardware de la
* videoconsola. Comprueba qué botones se han pulsado y si el
* usuario ha pulsado algo con el Stylus
* @param elementoPulsado devuelve el identificador del elemento
* que se ha pulsado. -1 si no se ha pulsado nada
* @param tipoElementoPulsado devuelve el tipo de elemento que
* se ha pulsado. -1 si no se ha pulsado nada
*/
void actualizarEventos(int *elementoPulsado, int
*tipoElementoPulsado);
/**
* Actualiza la acción que está realizando el personaje en
* función de los eventos que hayan sucedido y el transcurso de
* la partida
* @param elementoPulsado devuelve el identificador del elemento
* que se ha pulsado. -1 si no se ha pulsado nada
* @param tipoElementoPulsado devuelve el tipo de elemento que
* se ha pulsado. -1 si no se ha pulsado nada
*/
void actualizarAccion(int *elementoPulsado, int
*tipoElementoPulsado);
/**
* Actualiza el estado de la escena en función de la acción y
* los eventos sucedidos
* @param elementoPulsado devuelve el identificador del elemento
* que se ha pulsado. -1 si no se ha pulsado nada
* @param tipoElementoPulsado devuelve el tipo de elemento que
* se ha pulsado. -1 si no se ha pulsado nada
* @return devuelve true si el Escenario ha llegado a su fin,
* false si todavía no ha finalizado
*/
bool actualizarEstado(int *elementoPulsado, int
*tipoElementoPulsado);
/**
* Actualiza los textos que se muestran en la pantalla para
* informar al usuario del estado actual de las acciones que
* toma
* @param elementoPulsado devuelve el identificador del elemento
* que se ha pulsado. -1 si no se ha pulsado nada
/**
* Función invocada en actualizarEventos cuando se realiza una
* pulsación con el stylus que puede desembocar
* en un nuevo desplazamiento o en la acción sobre un objeto
* @param elementoPulsado devuelve el identificador del elemento
* que se ha pulsado. -1 si no se ha pulsado nada
* @param tipoElementoPulsado devuelve el tipo de elemento que
* se ha pulsado. -1 si no se ha pulsado nada
* @see actualizarEventos
*/
void actualizarStylus(int *elementoPulsado, int
*tipoElementoPulsado);
/**
* Comprueba los botones pulsados y actualiza el estado
* @see actualizarEventos
*/
void actualizarBotones();
/**
* Pinta todo el escenario: suelo, personaje y elementos.
*/
void pintarGL(){
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f,altura,distancia);
glRotateX(inclinacion);
glPushMatrix();
gluLookAt(
cam[0][0], cam[0][1], cam[0][2], // posición cámara
cam[1][0], cam[1][1], cam[1][2], // punto de mira
cam[2][0], cam[2][1], cam[2][2]); // vector alzado
Suelo.pintarGL();
Protagonista.pintarGL();
for(unsigned int
i=0;i<elementosNoSeleccionables.size();i++){
elementosNoSeleccionables[i].pintarGL();
}
glPopMatrix(0);
glFlush(0);
}
/**
* Pasada una coordenada (x,y) devuelve la cara de Suelo pulsada
* @param x coordenada eje x
* @param y coordenada eje y
* @param tipo tipo de elemento pulsado, puede ser SUELO o
* ENTIDAD
* @see startCheck()
* @see endCheck()
* @return cara del escenario pulsada con el Stylus
*/
int zonaPulsada(int x, int y, int *tipo);
/**
* Función usada por zonaPulsada.
* Ejecutada antes de dibujar un objeto durante el recorte.
* Guarda en una variable global el número de polígonos
* dibujados hasta el momento.
* @see zonaPulsada()
* @see endCheck()
*/
void startCheck();
/**
* Función usada por zonaPulsada.
* Ejecutado después de dibujar un objeto durante el recorte.
* Comprueba si el número de polígonos ha aumentado desde la
* llamada a startCheck, sabiendo entonces que en el área
* indicada por zonaPulsada había una cara.
* @see zonaPulsada()
* @see startCheck()
*/
void endCheck(int obj);
/**
* Función usada en el constructor Escenario para detectar las
* áreas contiguas. Se le pasa una coordenada (x,y) en un vector
* v y comprueba si está contenida dentro de alguna de las caras
* desplazables del suelo.
* @param v vector con coordenada a comprobar si pertenece a
* alguna cara desplazable
* @see Escenario()
* @return cara donde está contenido el vector v
*/
int perteneceArea(vector2 v);
/**
* Función usada en el constructor Escenario para detectar las
* áreas contiguas. Se le pasa una coordenada (x,y) en un vector
* v y comprueba si está contenida dentro de alguna de las caras
* desplazables del suelo.
* @param x coordenada x a comprobar si pertenece a álguna cara
* desplazable
* @param y coordenada y a comprobar si pertenece a álguna cara
* desplazable
* @see Escenario()
* @return cara donde está contenido el vector v
*/
int perteneceArea(float x, float y){
return perteneceArea(vector2(x,y));
}
/**
* Función usada en el constructor de Escenario.
* Comprueba que el elemento x está en el vector v
* @param x elemento a comprobar que existe en v
* @param v vector de elemento
* @return si existe devuelve la posición del elemento x en v,
* sino -1
*/
template<class T>
int esta(T x, vector<T> v);
/**
* Busca el camino entre un índice inicio y otro destino del
* suelo. Este algoritmo devuelve el primer camino que encuentra
* entre dos puntos
* @param indIni índice del nodo inicio
* @param indDest índice del nodo destino
* @param c camino actual
* @see devuelveCamino
* @see buscarCaminoAEstrella_Original
* @see buscarCaminoAEstrella_ModificadoOptimo
* @return variable booleana que indica si se ha encontrado el
* camino
*/
bool buscarCamino(int indIni, int indDest, vector<int>* c);
/**
* Busca el camino entre un índice inicio y otro destino del
* suelo. Este algoritmo devuelve el primer camino que encuentra
* entre dos puntos usando la heurísitica del camino euclídeo
* entre dos puntos
* @param indIni índice del nodo inicio
* @param indDest índice del nodo destino
* @param fin camino actual
* @see devuelveCamino
* @see buscarCamino
* @see buscarCaminoAEstrella_ModificadoOptimo
*/
void buscarCaminoAEstrella_Original(int indIni, int indDest,
vector<int>* fin);
/**
* Busca el camino entre un índice inicio y otro destino del
* suelo. Este algoritmo es una modificación del algoritmo A*
* que devuelve el camino más corto entre dos puntos
* @param indIni índice del nodo inicio
* @param indDest índice del nodo destino
* @param fin camino actual
* @see devuelveCamino
* @see buscarCaminoAEstrella_Original
* @see buscarCamino
*/
void buscarCaminoAEstrella_ModificadoOptimo(int indIni, int
indDest, vector<int>* fin);
/**
* Función que determina el coste entre dos nodos del suelo.
* Es parte del algoritmo A*
* @param startLoc localización de inicio
/**
* Función auxiliar para dado un vector de nodos y un
* identificados devuelve el nodo que tiene ese id
* @param nodos vector de nodos
* @param idNodo identificador del nodo a buscar
* @return puntero al nodo encontrado
*/
nodo* encuentraNodoPorId(vector<TNodo*> *nodos, int idNodo);
/**
* Función auxiliar del algoritmo A*
* Inserta un nodo n en el vector de nodos v
* @param nodos vector de nodos necesario para la consultad del
* id del nodo
* @param n nodo que se desea insertar
* @param v vector donde insertar el nodo en orden
*/
void insertar_en_orden(vector<TNodo*> *nodos, nodo
*n,vector<int> *v);
/**
* Devuelve el índice correspondiente a la posición de la
* caraDesplazable pasada por parámetro
* @param caraDesplazable cara desplazable del Escenario
* @return índice de la cara desplazable sobre el array
* carasDesplazables
*/
int devuelveIndice(int caraDesplazable);
/**
* Devuelve la coordenada del centro de la cara desplazable
* @param cara identificador de la cara que se desea conocer
* su centro
* @return coordenada (x,y) con el centro de la cara
*/
vector2 devuelveCentroArea(int cara);
/**
* Acción ejecutada al usar un elemento en la partida
* @param obj1 objeto usado
* @param obj2 (opcional) objeto sobre el que se usa el objeto
* obj1. Si este es NULL entonces se habrá usado solamente el
* obj1
* @return devuelve true si usar este objeto implica finalizar
* el escenario false en caso contrario
*/
virtual bool usar(Entidad *obj1, Entidad *obj2) = 0;
/**
* Acción ejecutada al recoger un elemento en la partida
* @param obj1 objeto a recoger
* @return devuelve true si usar este objeto implica finalizar
* el escenario false en caso contrario
*/
/**
* Esta función es invocada cuando se finaliza una animación
* para realizar los eventos que puedan ocurrir posteriormente
* @param obj1 objeto que ha finalizado su animación
* @return devuelve true si usar este objeto implica finalizar
* el escenario false en caso contrario
*/
virtual bool eventoFinalizarAnimacion(Entidad *obj1) = 0;
};
/**
* Constructor
* @param _suelo datos binarios que contiene los datos de la
* Entidad Suelo
* @param _textura datos binarios que contiene la textura del
* suelo
* @param _tamanoText tamaño de la textura
* @param _inc grados de inclinación del suelo
* @param _dist distancia de representación en el eje Z
* @param _alt altura de representación en el eje Y
*/
Escenario(const void * _suelo, const void * _textura, int
_tamanoText, float _inc, float _dist, float _alt);
Al crear este objeto se pide especificar el suelo. Hasta ahora no se ha explicado cómo se
define un suelo. Este componente consiste en un objeto en 3D como ya se ha visto que se
puede crear, pero con algunas particularidades. A la hora de definir este objeto se deben
cumplir los siguientes requisitos:
• La superficie puede tener la forma que se desee, por ejemplo en la figura 29 presenta una
forma serpenteante, pero debe estar compuesta por dos partes planas y paralelas, una
superior que será sobre la que se desplace el protagonista, y una inferior que nunca se verá
ya que será la parte trasera del suelo.
• La cara superior debe estar por encima del plano Y=0, es decir que tengan la
componente Y de altura positiva, y todos los puntos que formen el plano horizontal
superior deben estar a la misma altura.
• La cara inferior debe estar por debajo del plano Y=0, es decir que tengan la componente
Y de altura negativa, y todos los puntos que formen el plano horizontal inferior deben
estar a la misma altura.
• Las caras deben formar triángulos.
La imagen siguiente muestra un ejemplo de modelo 3D que sirve como suelo por cumplir las
condiciones.
Los demás componentes que necesita el constructor son la textura del suelo, y tres parámetros
decimales que indican la inclinación, la distancia y la altura con las que se debe mostrar el
escenario.
La parte más importante dentro del constructor es la de interpretar la disposición del suelo,
distinguir las zonas que son desplazables por el protagonista y obtener para cada cara
desplazable una lista de sus caras contiguas. Con esta técnica esta aplicación podrá utilizar
cualquier suelo que cumpla las reglas antes descritas, de ahí su importancia. Iremos viendo
paso a paso que acciones realiza esta tarea de constructor. Primero inicializa las variables
oportunas:
// Inicialización
estado=ESTADO_PARADO;
accion = ACCION_IR;
entidadSeleccionada = -1;
tiempoMensajePantalla = -1;
seleccionInventario = -1;
inclinacion = _inc;
distancia = _dist;
altura = _alt;
El siguiente paso es identificar las caras por las que se desplaza el personaje protagonista:
float x = v.x;
float y = v.y;
return cara;
}
El parámetro que recibe es una coordenada (x,y) que se consulta en el listado de caras
desplazables creado anteriormente. Si dicha coordenada está contenida dentro de alguna cara.
Se recuera que todas las caras deben ser triángulos (se trataba de un requisito que debía
cumplir el suelo), entonces por cada cara se rescatan los tres vértices (x,y) que la componen.
En las variables l1, l2, l3 se calcula en qué lado se encuentra el punto que se quiere
consultar con respecto a los tres vectores que se pueden formar restando los tres vértices de la
cara uno a uno. La única posibilidad que existe de que la coordenada que se busca, se
encuentre al mismo lado de cada uno de estos vectores, es cuando la coordenada está
contenida dentro del triángulo que forma la cara. Vista esta función auxiliar, se retoma la
explicación del método constructor que se iba a encargar de recoger las caras contiguas:
int c=-1;
vector<int> listaContiguas;
for(unsigned int i=0; i<carasDesplazables.size(); i++){
c=carasDesplazables[i];
listaContiguas.clear();
if(esta(areaContigua, listaContiguas)==-1)
listaContiguas.push_back(areaContigua);
cont[i]=vector<int>(listaContiguas);
}
while (!Escena->actualizar())
{
PA_WaitForVBL();
}
Este es el bucle de juego principal. Se trata de un bucle while que llama continuamente al
método actualizar de la clase Escenario, ya que Escena es una instancia de
Escenario. Mientras el resultado de actualizar no devuelva un valor false, el
escenario continúa ejecutando el ciclo. Visto esto, pasamos a explicar el contenido del método
actualizar del la clase Escenario. La idea es que en este método se atienden todas las
necesidades del ciclo de vida del juego. Veamos primero la implementación:
bool Escenario::actualizar()
{
// limpia la pantalla
PA_ClearTextBg(1);
// inicializa variables
int elementoPulsado[1], tipoElementoPulsado[1];
bool finEscenario = false;
finEscenario =
eventoFinalizarAnimacion(entidadEnAnimacion);
entidadEnAnimacion = NULL;
}
}
else
{
*elementoPulsado = *tipoElementoPulsado = -1;
actualizarEventos(elementoPulsado, tipoElementoPulsado);
actualizarAccion(elementoPulsado, tipoElementoPulsado);
finEscenario = actualizarEstado(elementoPulsado,
tipoElementoPulsado);
actualizarTextos(elementoPulsado, tipoElementoPulsado);
}
pintarGL();
return finEscenario;
}
Como siempre empieza inicializando las posibles variables que son necesarias, además de
limpiar los mensajes que se muestran por pantalla. Podemos ver tres variables:
elementoPulsado, tipoElementoPulsado y finEscenario. Las dos primeras se
usan para conocer si el usuario ha utilizado el lápiz o Stylus para pulsar sobre la pantalla. La
otra variable booleana será la que se devuelva al final del método para indicar si el escenario
ha finalizado, y debe pasar entonces al siguiente escenario o bien finalizar el juego. Tras esto
se distinguen dos posibles situaciones: que en este instante alguna entidad esté realizando una
animación, por ejemplo que se esté desplazando una mesa. Si esto ocurre, no pueden
atenderse eventos de ningún tipo hasta que no finalice la animación. Cuando esto sucede se
observa que se llama al método eventoFinalizarAnimacion que en la definición de
esta clase se declaró como un método virtual. Esto se debe a que las actuaciones que se
puedan tomar después de finalizar una animación, serán diferentes dependiendo de en qué
escenario se encuentra. Por ejemplo, si estamos en la primera pantalla de la habitación,
cuando se termine la animación de desplazar una mesa, se deberá desbloquear ciertos objetos;
mientras que si estamos en otra fase distinta, cuando se termine la animación de otro objeto
diferente las actuaciones no tendrán nada que ver. Por tanto, con esto se deja entender que la
clase Escenario que estamos explicando sólo se encargará de aspectos genéricos para
todos los juegos, las partes específicas de cada aventura deben ser implementadas en clases
que hereden de las existentes e implementen los métodos virtuales de manera oportuna. En el
punto 6.2.7 se atienden a este tipo de clases y se explica cómo implementar estos métodos
específicos entre los que se encuentra eventoFinalizarAnimacion. Por tanto, la otra
bifurcación del código sucede cuando no se está realizando ninguna animación. En este caso
si se atienden a los posibles eventos y se realiza el ciclo de juego de manera normal. En
primer lugar, se llama al método actualizarEventos que lee cualquier posible evento
que haya podido lanzar el usuario. Este método llama a dos métodos a su vez:
actualizarBotones y actualizarStylus. La implementación del primero se
muestra a continuación:
void Escenario::actualizarBotones(){
entidadSeleccionada = -1;
tiempoMensajePantalla = -1;
if(Pad.Held.Y){
accion = ACCION_IR;
}
else if(Pad.Held.A){
accion = ACCION_USAR;
}
else if(Pad.Held.B){
accion = ACCION_RECOGER;
}
En primer lugar se realiza una acción para mantener la consistencia, y es que si estaba pulsada
la acción usar, entonces el usuario podía haber dejando marcado algún objeto del inventario,
por tanto si cambia a otro tipo de acción por ejemplo recoger o ir a, debe dejar de estar
seleccionado algún elemento del inventario. Después se comprueba si se ha pulsado algún
botón del teclado para cambiar a la acción correspondiente. Por último si la acción que se ha
pulsado era la de girar la cámara, se actúa en consecuencia girándola dentro de los límites
posibles. Tras este método, actualizarEventos llama a otro, actualizarStylus.
Primero la implementación de este método:
if(Stylus.Held){
entidadSeleccionada = -1;
tiempoMensajePantalla = -1;
Se comprueba que el Stylus ha sido pulsado con Stylus.Held, en tal caso se hace una
llamada a zonaPulsada pasándole la posición (x,y) sobre la que está presionando el
puntero, además de un puntero y otro que devuelve. Con esto se consigue conocer qué tipo de
elemento se ha pulsado y su identificador para poder reconocerlo cuando sea necesario. Los
tipos de elementos están definidos en constantes y pueden ser:
/**
* Se devuelve esta constante cuando el usuario pulse con el puntero
* sobre el suelo
* @see actualizarStylus
*/
#define SELECCION_SUELO 0
/**
* Se devuelve esta constante cuando el usuario pulse con el puntero
* sobre una entidad
* @see actualizarStylus
*/
#define SELECCION_ENTIDAD 1
/**
* Se devuelve esta constante cuando el usuario no pulse ni sobre el
* suelo ni sobre una entidad
* @see actualizarStylus
*/
#define SELECCION_AIRE 2
Las acciones que lleva a cabo la función zonaPulsada son las de identificar qué elemento
de la escena ha sido pulsado. Esto se explicó en el apartado 3.3.7 cuando se comentaba como
se podían usar las librerías empleadas. La técnica aquí empleada es idéntica a la explicada,
simplemente que aquí hay que diferenciar entre si el elemento pulsado es de tipo suelo o una
entidad. Esto no tiene demasiada complicación. Si se recuerda cómo funcionaba este
algoritmo, iba recortando la escena alrededor de la zona que se indica que se ha pulsado con
el puntero, y pintando elemento a elemento, antes de pintar cada uno comprobaba el número
de polígonos pintados y luego de pintar comprobaba si esa cantidad había variado, en tal caso
deducía que se había pulsado el elemento pulsado. Lo que hacemos ahora es diferenciar en
dos etapas cuándo estamos comprobando si hemos pulsado el suelo y cuándo los elementos.
Si el número de polígonos aumenta en la primera etapa o en la segunda, ya sabremos qué
hemos pintado.
if(*elementoPulsado != -1){
// si la acción es ir y se ha pulsado un
// elemento entonces se ilumina, se calcula el
// camino y se cambia el estado a
// desplazamiento
if(*tipoElementoPulsado == SELECCION_ENTIDAD){
// Se ilumina solamente la Entidad que
// haya sido seleccionada
for(int e=0;
((unsigned int) e) <
elementosSeleccionables.size();
e++){
elementosSeleccionables[e]->
iluminar =
(e==*elementoPulsado);
}
this->camino = camino;
entidadSeleccionada = *elementoPulsado;
for(int e=0;
((unsigned int) e) <
elementosSeleccionables.size();
e++){
elementosSeleccionables[e]->
iluminar = false;
}
vector<int> camino =
devuelveCamino
(situacionPersonaje() ,
*elementoPulsado);
this->camino = camino;
estado = ESTADO_DESPLAZAMIENTO;
}
}
break;
case ACCION_RECOGER:
// si la acción es recoger y se ha
// pulsado una entidad se ilumina,
// calcula el camino y se desplaza
// hacia ella
if(*elementoPulsado != -1 &&
*tipoElementoPulsado == SELECCION_ENTIDAD){
for(int e=0;
((unsigned int)e) <
elementosSeleccionables.size();
e++){
elementosSeleccionables[e]->iluminar =
(e==*elementoPulsado);
}
vector<int> camino =
devuelveCamino(situacionPersonaje(),
elementosSeleccionables
[*elementoPulsado]-> situacionEntidad());
this->camino = camino;
entidadSeleccionada = *elementoPulsado;
estado = ESTADO_DESPLAZAMIENTO;
}
break;
case ACCION_USAR:
// Actualiza la posición de
// la selección del inventario
if(Pad.Newpress.Down)
{
seleccionInventario++;
}
else if(Pad.Newpress.Up)
{
seleccionInventario--;
}
vector<int> camino =
devuelveCamino(situacionPersonaje(),
elementosSeleccionables
[*elementoPulsado]->situacionEntidad());
this->camino = camino;
entidadSeleccionada = *elementoPulsado;
estado = ESTADO_DESPLAZAMIENTO;
}
break;
}
}
entidad para como hemos hecho hasta ahora, iluminarla, encontrar el camino y cambiar el
estado.
switch(estado)
{
case ESTADO_PARADO :
break;
case ESTADO_DESPLAZAMIENTO :
// ESTADO_DESPLAZAMIENTO se centra
// en ir recorriendo el camino hasta
// el destino
// inicializa el camino
// ya recorrido
camino = vector<int>();
// el protagonista se detiene
// y por tanto vuelve al frame original
Protagonista.fr=0;
if(!ent.puedeRecogerse){
sprintf(mensajePantalla,
ent.mensajeNoRecoger);
tiempoMensajePantalla =
TIEMPO_MENSAJE_PANTALLA;
}
else{
for(int e=0;
((unsigned int)e)
<elementosSeleccionables.size();
e++){
elementosSeleccionables[e]->
iluminar = false;
}
inventario.push_back(ent);
finEscenario = recoger(&ent);
desvincularElementoSeleccionable
(ent.nombre);
}
}
if(seleccionInventario != -1)
{
entInventario =
&inventario[seleccionInventario];
}
entidadSeleccionada = -1;
// distancia a recorrer
float distancia = orig.distancia(sig);
float distX = orig.x-sig.x, distY = orig.y-sig.y;
float despY =
distY/(floorf(distancia/velocidadDesplazamiento)+1);
Protagonista.instante.pos[0] = sig.x;
Protagonista.instante.pos[2] = sig.y;
camino.erase(camino.begin(),camino.begin()+1);
}
else{
break;
}
return finEscenario;
}
para que haga el efecto de seguir al personaje. La última línea devuelve la variable booleana
que indica si el escenario ha terminado, según la respuesta de los métodos virtuales usar y
recoger.
switch(accion){
case ACCION_IR :
PA_SetSpriteXY(1,0,137,57);
sprintf(mensajePantalla, "Ir a");
if(entidadSeleccionada != -1){
sprintf(mensajePantalla, "%s %s",
mensajePantalla,
elementosSeleccionables
[entidadSeleccionada]->nombre);
}
break;
case ACCION_RECOGER :
PA_SetSpriteXY(1,0,171,101);
sprintf(mensajePantalla, "Recoger");
if(entidadSeleccionada!=-1)
{
sprintf(mensajePantalla, "%s %s",
mensajePantalla,
elementosSeleccionables
[entidadSeleccionada]->nombre);
}
break;
case ACCION_USAR :
PA_SetSpriteXY(1,0,203,57);
if(seleccionInventario != -1)
{
PA_SetSpriteXY(1,1,12,
seleccionInventario*24+46);
PA_SetSpriteXY(1,2,32+12,
seleccionInventario*24+46);
//Ajusto el último sprite pq el
// ancho total son 92 px, no 96 px (32*3)
PA_SetSpriteXY(1,3,32*2+8,
seleccionInventario*24+46);
}
else
{
for(int i=0; i<3; i++)
{
PA_SetSpriteX(1,i+1,255);
}
}
sprintf(mensajePantalla, "Usar");
if(seleccionInventario != -1 &&
entidadSeleccionada == -1)
{
sprintf(mensajePantalla, "%s %s con",
mensajePantalla,
inventario[seleccionInventario]
.nombre);
}
if(seleccionInventario != -1 &&
entidadSeleccionada != -1)
{
sprintf(mensajePantalla, "%s %s con %s",
mensajePantalla,
inventario[seleccionInventario].
nombre,
elementosSeleccionables
[entidadSeleccionada]->nombre);
}
else if(entidadSeleccionada != -1)
{
sprintf(mensajePantalla, "%s %s",
mensajePantalla,
elementosSeleccionables
[entidadSeleccionada]->nombre);
}
break;
}
}
// si se debe seguir mostrando un mensaje,
// se decrementa el tiempo que debe seguir
// mostrándose
else
{
tiempoMensajePantalla--;
}
// se pinta el mensaje
PA_OutputText(1,5,22,mensajePantalla);
}
Existe una variable que indica si un mensaje se debe imprimir más de un cierto tiempo en
pantalla, por ejemplo para advertir de algo al usuario. Esta es tiempoMensajePantalla
y mientras tenga valor mayor de -1, entonces se mantiene el mensaje que se estaba pintando
hasta que decremente lo suficiente para que deje de cumplirse esa condición. Cuando su valor
llegue a -1, entonces se comprueba cual es la acción que está activada. En el caso de
ACCION_IR el mensaje a mostrar es "Ir a (elemento seleccionado)". Lo que está entre
paréntesis es para indicar que es opcional. Mientras no se hay seleccionado ninguna entidad
pero se tenga activada esta acción solamente se mostrará “Ir a”. En el momento que se pulse
sobre un objeto seleccionable entonces aparecerá el mensaje completo, por ejemplo “Ir a
mesa”. De forma similar ocurre si la acción es ACCION_RECOGER con el mensaje “Recoger
(elemento seleccionado)”. La acción ACCION_USAR puede tener diversos formatos, en
función de si hay seleccionado un elemento de la escena o si se selecciona un elemento del
escenario. El mensaje más completo es Usar (elemento seleccionado) con (elemento
inventario)". Dónde el primer elemento aparecerá escrito según si se selecciona algún
componente con el puntero o Stylus, y el segundo dependerá de la selección que se efectúe en
el inventario. Por último se pintan los nombres de los elementos que contiene el usuario en el
inventario y después el mensaje previamente construido.
La última llamada que realiza el método actualizar es pintarGL que se encarga de pintar la
escena una vez actualizada. La implementación estaba en el fichero de definición ya copiado,
pero lo recuperamos de nuevo:
void pintarGL(){
// inicializa la escena
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f,altura,distancia);
glRotateX(inclinacion);
glPushMatrix();
{
// posiciona la cámara
gluLookAt(
// posición cámara
cam[0][0], cam[0][1], cam[0][2],
// punto de mira
cam[1][0], cam[1][1], cam[1][2],
// vector alzado
cam[2][0], cam[2][1], cam[2][2]);
// pinta al protagonista
Protagonista.pintarGL();
}
glPopMatrix(0);
glFlush(0);
}
Aquí se pintan todos los componentes. Primero se inicializan los valores oportunos, a
continuación se posiciona la cámara, luego se pinta el suelo, el protagonista y los elementos
seleccionables y no seleccionables. Lo siguiente que podemos ver es la manera de pintar una
entidad:
void Entidad::pintarGL(){
glEnable(GL_TEXTURE_2D);
glColor3b(255,255,255);
// inicializa
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(30, 256.0 / 192.0, 0.1, 20);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
{
// se calcula el cuaternion de la rotación
glQuaternion q(
f[i].a[fr].rot[1],
f[i].a[fr].rot[2],
f[i].a[fr].rot[3],
f[i].a[fr].rot[0]);
float angle=2.0f*acos(q.m_w)*180.0f/PI;
float s=sqrt(1-q.m_w*q.m_w);
float x, y, z;
instante.esc[1],
instante.esc[2]);
glBegin(GL_TRIANGLE);
int c=0;
for(int vC=0; vC<vecMat; vC++)
{
c++;
}
glEnd();
}
}
glPopMatrix(1);
}
}
Este método pintarGL es el mismo para todas las entidades. Primero se inicializan algunos
valores. Se activa la iluminación en el contorno si fuera necesario o se desactiva en el caso
contrario. El siguiente bucle pinta cada figura que compone la Entidad en concreto. Cada
una puede estar sometida a dos transformaciones, una la propia del instante en el que se
encuentra, por ejemplo girada según la dirección del movimiento, y otra transformación según
el frame de su animación, por ejemplo la animación de desplazamiento del personaje. Lo
primero que se hace es calcular el ángulo de la transformación de rotación de la animación a
partir del cuaternión. Después se aplica la transformación del instante, luego la de la
animación para finalmente pintar todas las caras de la figura aplicándole la textura del
material correspondiente.
return camino;
}
En este caso vamos a implementar un algoritmo A* modificado para encontrar el camino más
corto, sin embargo también se puede optar por decisiones más rápidas como ir explorando el
espacio hasta encontrar la primera solución posible.
algoritmo examina repetidamente el nodo más “prometedor” sin explorar. Cuando alcanza un
nodo, acaba si este era su objetivo, o en otro caso anota los nodos colindantes para continuar
explorando.
Este algoritmo es una mejora de los algoritmos voraces ya que hace uso de una función
distancia-más-coste heurística f(x) pero la usa para determinar el orden en el que visitar los
nodos, en lugar de fiarse de su resultado exclusivamente para realizar el siguiente paso de
exploración como es habitual en los algoritmos de búsqueda voraz. La función f(x) de nuestro
algoritmo se descompone en dos funciones, una función de coste g(x) que puede ser heurística
o no, y una función de estimación heurística h(x) que da la distancia al objetivo por una
aproximación sencilla.
En más detalle, el algoritmo A* mantiene dos listas de estados, llamadas open y close que van
almacenando los estados sin examinar y los examinados respectivamente. Al comienzo del
algoritmo, close está vacía y open tiene solamente el nodo de inicio. En cada iteración, el
algoritmo elimina el estado más prometedor de open para examinarlo. Si el estado no es el
objetivo, se ordenan los estados contiguos: si son nuevos, entonces se sitúan en open; si ya
estaban en open, entonces se actualiza su información si son una ruta más corta; si ya estaban
en close se ignoran porque ya habían sido examinados. Si la lista open se encuentra vacía
antes de alcanzar el objetivo, significa que no existe un camino hasta el objetivo desde la
localización origen.
El nodo más prometedor en open es la localización con menor coste estimado. Cada estado
incluye información que determina: el coste de la menor ruta desde el comienzo,
CostFromStart (g(x)); una heurística estimada, CostToGoal (h(x)), del coste aproximado de la
distancia restante al objetivo; y el camino total estimado, definido como CostFromStart(g(x))
+ CostToGoal (f(x)). El nodo con menor camino total estimado será el próximo en examinar.
Además, cada nodo contiene un puntero a su nodo padre, parent hacia el camino más corto,
para poder reconstruir posteriormente dicho camino cuando se alcance el objetivo. A
continuación se muestra la estructura de datos utilizada para la definición anterior:
Lo siguiente sería realizar la implementación de este algoritmo, sin embargo no cumple con
todas las necesidades que harían falta. Ya que la búsqueda que va haciendo este algoritmo
asegura ir recorriendo el espacio de estados por los nodos más prometedores basándose en
función de aproximación heurística además de la información que va anotando de todos los
nodos que recorre para realizar la mejor elección. Sin embargo, en el momento que encuentra
el nodo objetivo, finaliza devolviendo la solución. Esto puede llevar a que en algunas
ocasiones no devuelva el camino más corto, sino el más corto de los que ha explorado. Por
tanto, la implementación por la que se ha optado ha sido una adaptación de este algoritmo,
que consiste en no finalizar al encontrar el nodo destino, sino seguir buscando hasta que se
acabe la lista de open, y en ese momento consultar la lista de nodos visitados en close para
elegir el camino con menor coste de entre los encontrados. Una vez explicado esto, se muestra
la implementación:
// Inicialización
for(unsigned int i=0; i<carasDesplazables.size(); i++)
{
nodo *n = (nodo*)malloc(sizeof(TNodo));
n->costFromStart=MAX;
n->loc = carasDesplazables[i];
n->indice = devuelveIndice(n->loc);
nodos.push_back(n);
}
fin->clear();
// nodo inicial
nodo *startNode=encuentraNodoPorId(&nodos, startLoc);
startNode->costFromStart=0;
startNode->costToGoal=PathCostEstimate(startLoc,goalLoc);
startNode->parent=NULL;
open.push_back(startNode->loc);
nodo *newNode;
{
insertar_en_orden(&nodos,newNode,&open);
}
// sino se inserta en la cabeza
else
{
open.push_back(newNode->loc);
}
}
} //for
vector<int> f;
// se recupera el nodo objetivo y a partir
// de él se reconstruye el camino
nodo *ptr=encuentraNodoPorId(&nodos, goalLoc);
while(ptr->parent!=NULL)
{
f.push_back(ptr->loc);
ptr=ptr->parent;
}
f.push_back(ptr->loc);
return;
}
Por tanto, para crear la aventara es necesario heredar de las clases Entidad y Escenario
para especializarlas según los criterios impuestos por el diseñador de juego. A continuación se
aborda cada clase a heredar por separado.
#ifndef ENTIDAD_NOMBRE_H
#define ENTIDAD_NOMBRE_H
#include "Entidad.h"
#include "modelo3D.h"
#include "textura.h"
~NombreEntidad(){};
};
#endif
Los valores que aparecen en negrita son para indicarlos manualmente en cada entidad.
ENTIDAD_NOMBRE_H es para utilizar las macros de C #ifndef y #define, puede ser
cualquier nombre aunque lo normal es algo identificativo. La palabra modelo3D es para
indicar el nombre del fichero exportado desde el formato X, que deberá tener extensión .bin
al haber sido creado con la herramienta implementada en esta memoria. textura debe ser el
nombre del fichero que almacena la textura del objeto. El nombre de la clase Entidad lo
definimos en NombreEntidad. La última palabra por declarar es tamañoTextura que
como indica, es el tamaño de la textura y debe ser 64 ó 128 (es decir que la textura sería de
64x64 ó 128x128). Es importante decir que pueden existir problemas si se usan varias
texturas con el tamaño de 128 ya que la videoconsola dispones de un límite de memoria para
texturas, por tanto se recomienda no usar más de 2 texturas de 128.
#ifndef ESCENARIO_NOMBRE_H
#define ESCENARIO_NOMBRE_H
#include "Escenario.h"
virtual ~NombreEscenario(){};
#endif
NombreEscenario::NombreEscenario(Personaje *_Prota):
Escenario(objetoSuelo, texturaSuelo,
tamañoTexturaSuelo, inclinación,
distancia, altura)
{
// ascocia al protagonista
Prota = _Prota;
vincularElementoSeleccionable(EntidadHeredada1, posiciónX1,
posiciónY1, posiciónZ1);
vincularElementoSeleccionable(EntidadHeredada2, posiciónX2,
posiciónY2, posiciónZ2);
vincularElementoSeleccionable(EntidadHeredadan, posiciónXn,
posiciónYn, posiciónZn);
vincularElementoNoSeleccionable(EntidadHeredada2, posiciónX2,
posiciónY2, posiciónZ2);
vincularElementoNoSeleccionable(EntidadHeredadan, posiciónXn,
posiciónYn, posiciónZn);
Se distinguen varios pasos. Primero en el constructor se le deben pasar el nombre del objeto
3D en objetoSuelo correspondiente al suelo, convertido desde un fichero X como ya se ha
visto; además se pasa su textura texturaSuelo y el tamaño de esta
tamañoTexturaSuelo (un valor de 64 ó 128). También se le pasan los valores de
inclinación, distancia y altura con los que representar el suelo. Después se van a
vincular los elementos que forman la aventura a las variables que se crearon en el fichero de
definición de esta clase. Estos son el protagonista y las entidades heredadas. Cuando ya están
relacionadas con las variables, es necesario vincularla a las entidades internas del escenario a
través de los métodos vincularProtagonista,
vincularElementoSeleccionable y vincularElementoNoSeleccionable.
En los dos últimos es necesario declarar el nombre del objeto Entidad y la coordenada
(x,y,z) dónde debe establecerse dentro del Escenario. Lo último que se indica en el
constructor son los mensajes que se muestran al usuario antes, y después de acceder a la
escena con el fin de mantener el hilo argumental. Se deben ir almacenando en un vector al que
más tarde se accede indicando qué mensajes se quieren mostrar. El siguiente método a
implementar por esta clase es recoger:
case MSJ_RECOGER_OBJETO_ESCENA:
sprintf(mensajePantalla, obj1->mensajeNoRecoger);
break;
}
if(mostrarMensaje != MSJ_NO_MOSTRAR)
{
tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA;
}
return fin;
}
Este método da plena libertad al programador para realizar todo tipo de acciones después de
recoger un objeto. Por ejemplo, puede imaginarse un objeto bloqueado por tener otro objeto
encima, que al empujarlo queda liberado. Por tanto, el método comienza definiendo dos
variables: la primera se utiliza para indicar al sistema si tras la recogida del objeto no se debe
mostrar ningún mensaje al usuario o si en cambio se muestra un mensaje genérico o el
específico del objeto, la otra variable es la de fin de escenario que si se activa durante la
ejecución del método se dará por terminada esta fase. Después se encuentra una bifurcación
if/else if por cada uno de los objetos que puedan ser recogidos. Cada sentencia
selectiva comienza identificando el objeto que ha provocado la ejecución de este método para
realizar las acciones en consecuencia, una vez que se conozca cuál ha sido, en el código se
muestran posibles actuaciones que se pueden realizar por supuesto el programador tiene total
libertad para sus actuaciones hasta que su imaginación se lo impida.
// inicialización
// son:
// MSJ_NO_MOSTRAR: no muestra ningún mensaje después de
// esta actuación
// MSJ_USAR_GENERICO: muestra el mensaje genérico definido
// al final de este método
// MSJ_USAR_OBJETO_ESCENA: muestra el mensaje que se define
// de manera específica para el objeto a usar
// MSJ_USAR_OBJETO_INVENTARIO: muestra el mensaje que se define
// de manera específica para el objeto del inventario a usar
int mostrarMensaje = MSJ_USAR_GENERICO;
// variable que indica si usar el objeto hace finalizar el
// escenario
bool fin = false;
// posibles actuaciones:
// -> si se ha seleccionado un objeto del inventario
// y este es nombreEntidadHeredadai1 entonces se actúa
// de la siguiente manera
// if(obj2 != NULL && strcmp(obj2->nombre,
// "nombreEntidadHeredadai1") == 0)
// {
// actuaciones posibles. Ver actuaciones posibles del
// método recoger
// }
//
// else
// {
// mostrarMensaje = MSJ_USAR_OBJETO_ESCENA;
// }
}
else if(strcmp(obj1->nombre, "nombreEntidadHeredada2") == 0)
{
// realizar actuaciones pertinentes como por ejemplo:
if (
nombreEntidadHeredadai2->puedeUsarse &&
obj2 == NULL &&
poseeEnInventario("nombreEntidadHeredadai3") &&
strcmp(obj2->nombre, " nombreEntidadHeredadai3") == 0
)
{
nombreEntidadHeredadai4->puedeRecogerse = true;
nombreEntidadHeredadai5->puedeUsarse = false;
}
else if(!nombreEntidadHeredadai6->puedeUsarse)
{
mostrarMensaje = MSJ_USAR_GENERICO;
}
else
{
mostrarMensaje = MSJ_USAR_OBJETO_ESCENA;
}
}
else
{
mostrarMensaje = MSJ_USAR_GENERICO;
}
case MSJ_USAR_OBJETO_ESCENA:
sprintf(mensajePantalla, obj1->mensajeNoUsar);
break;
case MSJ_USAR_OBJETO_INVENTARIO:
sprintf(mensajePantalla, obj2->mensajeNoUsar);
break;
}
if(mostrarMensaje != MSJ_NO_MOSTRAR)
{
tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA;
}
return fin;
}
En este caso el método usar recibe dos parámetros, obj1 y obj2. El primero es el objeto que
el usuario haya pulsado con el Stylus sobre la pantalla, este puntero siempre apuntará a una
entidad, en cambio el segundo objeto es opcional y se trata del objeto que puede tener
seleccionado el usuario en el inventario. La utilidad de esto como ya se ha comentado es
poder combinar un objeto del inventario con otro objeto de la escena. Al igual que con el
método anterior existen dos variables que se inicializan, una para hacer entender al sistema
qué mensaje mostrar al usuario tras la ejecución de este método; la otra para indicar el final
del escenario tras esta actuación. Luego de manera similar se identifica que objeto se pretende
utilizar para realizar la secuencia de acciones necesaria, ahora es cuando además de conocer el
objeto que se ha pulsado del escenario, se puede comprobar si además el usuario pretende
combinarlo con un objeto del inventario para concretar las acciones a realizar. En la rama
else if de esta implementación se observa un ejemplo más completo de qué cosas se
pueden hacer. Primero se comprueba que el objeto del escenario es
nombreEntidadHeredada2 y luego se consulta que otra entidad, llamada por ejemplo
nombreEntidadHeredadai2 esté disponible para usar y además se quiera combinar la
entidad anterior, nombreEntidadHeredada2, con otra llamada
nombreEntidadHeredadai3 y que se encuentre en el inventario y es la segunda entidad
que se llama en este método. Las consecuencias de estas condiciones son que pueda usarse la
entidad nombreEntidadHeredadai4, pueda recogerse la
nombreEntidadHeredadai5, y se desemboque el inicio de la animación de otra entidad
nombreEntidadHeredadai6. Los pasos para que se ejecute una animación son asociar la
entidad en cuestión a la variable global entidadEnAnimacion, inicializar su frame a 0,
indicar en la variable global finAnimacion cuál es el último frame a ejecutar y establecer
que el estado debe ser ESTADO_ANIMACION. Con esto se consigue que tras la ejecución de
este método la escena pase a ejecutar la secuencia de animación del objeto que se apunte
desde entidadEnAnimacion hasta llegar al final indicando en la variable
finAnimacion. Cuando esto acabe se pasará a ejecutarse el siguiente método que vamos a
ver eventoFinalizarAnimacion por si quedan eventos que realizar, y luego volvería el
control del juego al usuario. Finalmente, el último método para definir una clase Escenario
personalizada, eventoFinalizarAnimacion:
return fin;
}
Al igual que en los métodos anteriores, en este se deja la libertad para que el desarrollador
cumpla la historia del juego insertando aquí el código necesario al finalizar el evento de la
entidad adecuada. Son importantes las dos últimas líneas ya que este método debe decidir si
realmente se ha terminado el estado de mostrar una animación, para lo cual debe establecerse
finAnimacion a -1 y estado a ESTADO_PARADO.
int main()
{
// inicializa las librerías PAlib
// y la interfaz del usuario que
// representa el menú
inicializarMenu();
// inicializa la escena 3D
inicializaEscena3D();
while(!Escena1->actualizar())
{
PA_WaitForVBL();
}
Escena1->mostrarTextoPantalla(5,7);
reiniciarEscena();
while (!Escena2->actualizar())
{
PA_WaitForVBL();
}
Escena2->mostrarTextoPantalla(0, 2);
return 0;
}
mucho conocimiento de esta estructura, pueda crear su propia historia basándose en ejemplos
como el que se va a implementar ahora.
6.3.1 Protagonista
De esta entidad ya se mostraron algunas capturas de su forma, sin embargo se vuelve a
mostrar tal como se va a hacer con el resto de entidades:
Del personaje en concreto no se crea ningún fichero de código ya que las interacciones que
pueda hacer se coordinan desde el escenario en concreto. Los elementos que si hace falta son:
animación de movimiento definida, fichero del modelo creado a partir del formato X y fichero
con la textura.
6.3.2 Entidades
Ahora, una a una se ven las implementaciones de las entidades que participan en la aventura,
primero se muestra una captura de su modelo 3D con su textura y luego los ficheros de código
que las definen en la aplicación.
6.3.2.1 Mesa
• Modelo 3D:
• EntidadMesa.h:
~EntidadMesa(){};
};
6.3.2.2 Estatua
• Modelo 3D:
• EntidadEstatua.h:
public:
puedeRecogerse = true;
};
~EntidadEstatua(){};
};
6.3.2.3 Puerta
• Modelo 3D:
• EntidadPuerta.h:
};
~EntidadPuerta(){};
};
6.3.2.4 Fregona
• Modelo 3D:
• EntidadFregona.h:
puedeRecogerse = true;
};
~EntidadFregona(){};
};
6.3.2.5 Llave
• Modelo 3D:
• EntidadLlave.h:
};
~EntidadLlave(){};
};
6.3.2.6 Árbol
• Modelo 3D:
• EntidadArbol.h:
sprintf(mensajeNoRecoger,"No se puede");
};
~EntidadArbol(){};
};
6.3.2.7 Coco
• Modelo 3D:
• EntidadCoco.h:
};
~EntidadCoco(){};
};
6.3.3 Escenarios
Pasamos a describir como se han desarrollado los dos escenarios en los que tiene lugar la
aventura.
• Suelo:
• Pared:
• EscenarioHabitacion.h:
EscenarioHabitacion(Personaje *_Prota);
virtual ~EscenarioHabitacion(){};
• EscenarioHabitacion.cpp:
EscenarioHabitacion::EscenarioHabitacion(Personaje
*_Prota):Escenario(suelo, sueloText, 64, 15.0f, -5.f, -1.2f)
{
// Enlaza al protagonista
Prota = _Prota;
vincularProtagonista(Prota,.0f,.0f,.0f);
vincularElementoSeleccionable(Fregona,4.3f,.2f,.7f);
vincularElementoSeleccionable(Puerta,2.2f,.2f,1.3f);
vincularElementoSeleccionable(Mesa,-1.8f,.2f,.8f);
vincularElementoSeleccionable(Estatua,-1.7f,.6f,.2f);
vincularElementoSeleccionable(Llave,-1.8f,.2f,.6f);
vincularElementoNoSeleccionable(HabitacionCompleta,.0f,.22f,.0f);
switch(mostrarMensaje)
{
case MSJ_RECOGER_GENERICO:
case MSJ_RECOGER_OBJETO_ESCENA:
sprintf(mensajePantalla, obj1->mensajeNoRecoger);
break;
}
if(mostrarMensaje != MSJ_NO_MOSTRAR)
{
tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA;
}
return fin;
}
if(strcmp(obj1->nombre, "puerta") == 0)
{
// si el protagonista usa la puerta, y tiene seleccionado
// en el inventario el objeto llave, entonces termina el
// escenario
if(obj2 != NULL && strcmp(obj2->nombre, "llave") == 0)
{
fin = true;
}
else
{
mostrarMensaje = MSJ_USAR_OBJETO_ESCENA;
}
}
// si se usa la mesa y ya ha recogido la
// estatua, entonces la llave pasa a
// poder recogerse y la mesa deja de poder
// usarse. En tal caso se inicia
// la animación de desplazamiento de
// la mesa para despejar el acceso
// hasta la llave
else if(strcmp(obj1->nombre, "mesa") == 0)
{
if (
Mesa->puedeUsarse &&
obj2 == NULL &&
poseeEnInventario("estatua")
)
{
Llave->puedeRecogerse = true;
Mesa->puedeUsarse = false;
switch(mostrarMensaje)
{
case MSJ_USAR_GENERICO:
case MSJ_USAR_OBJETO_ESCENA:
sprintf(mensajePantalla, obj1->mensajeNoUsar);
break;
case MSJ_USAR_OBJETO_INVENTARIO:
sprintf(mensajePantalla, obj2->mensajeNoUsar);
break;
}
if(mostrarMensaje != MSJ_NO_MOSTRAR)
{
tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA;
}
return fin;
}
return false;
• Suelo:
• EscenarioExterior.h:
EscenarioExterior(Personaje *_Prota);
virtual ~EscenarioExterior(){};
• EscenarioExterior.cpp:
EscenarioExterior::EscenarioExterior(Personaje *_Prota):Escenario(suelo3,
suelo2Text, 64, 15.0f, -5.f, -1.2f)
{
// Enlaza al protagonista
Prota = _Prota;
vincularProtagonista(Prota,.8f,.0f,-.5f);
vincularElementoSeleccionable(Arbol,-4.,.0f,.0f);
vincularElementoSeleccionable(Coco,-3.8,1.4f,-.3f);
/**
* TODO: realizar acciones oportunas al recoger los objetos
*/
switch(mostrarMensaje)
{
case MSJ_RECOGER_GENERICO:
case MSJ_RECOGER_OBJETO_ESCENA:
sprintf(mensajePantalla, obj1->mensajeNoRecoger);
break;
}
if(mostrarMensaje != MSJ_NO_MOSTRAR)
{
tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA;
}
return fin;
}
{
int mostrarMensaje = MSJ_NO_MOSTRAR;
bool fin = false;
switch(mostrarMensaje)
{
case MSJ_USAR_GENERICO:
case MSJ_USAR_OBJETO_ESCENA:
sprintf(mensajePantalla, obj1->mensajeNoUsar);
break;
case MSJ_USAR_OBJETO_INVENTARIO:
sprintf(mensajePantalla, obj2->mensajeNoUsar);
break;
}
if(mostrarMensaje != MSJ_NO_MOSTRAR)
{
tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA;
}
return fin;
}
PA_ClearTextBg(1);
PA_ShowBg(1,3);
// fin test
finAnimacion = -1;
estado = ESTADO_PARADO;
fin = true;
}
return fin;
}
int main()
{
// inicializa las librerías PAlib
// y la interfaz del usuario que
// representa el menú
inicializarMenu();
// inicializa la escena 3D
inicializaEscena3D();
while (!Escena->actualizar())
{
PA_WaitForVBL();
}
Escena->mostrarTextoPantalla(5,7);
reiniciarEscena();
while (!Escena2->actualizar())
{
PA_WaitForVBL();
}
Escena2->mostrarTextoPantalla(0, 2);
return 0;
}
Capítulo 7. Conclusiones
En este apartado a modo de colofón, se anotan los pensamientos que han ido surgiendo a lo
largo del desarrollo tanto de la aplicación como de la memoria. Esto abarca mejoras que se
han contemplado una vez comenzado el desarrollo, diferentes implementaciones que por
diversos motivos no se pudieron llevar a cabo o futuros desarrollos que pueden realizarse al
juego.
que mejoran de una manera dinámica gracias al sistema wiki. Sin embargo, para la aplicación
que se ha desarrollado, los principales bloqueos encontrados han sido que para algunos
aspectos concretos no se ha encontrado el soporte necesario. A veces por ser temas muy
concretos los que se han tratado, o en otras ocasiones por tener que desarrollarse soluciones
específicas que evidentemente no podían estar documentadas en ningún sitio, a lo sumo
ofreciendo orientación y opiniones.
El caso de la librería PAlib es el ejemplo de cómo una persona sin experiencia en desarrollo
en esta videoconsola puede recibir mucha información necesaria que le facilite el comenzar
poco a poco cada vez realizando pasos más complicados. Sin embargo, la otra librería en la
que PAlib se basa, libnds (recordamos que esta estaba enfocada a más bajo nivel de
abstracción y que era más compleja de utilizar) no existe tanta documentación. En principio
este desarrollo no se ha basado tanto en esta librería como en PAlib y por eso no había
problema, sin embargo una parte importante del proyecto si requería hacer uso de libnds.
Concretamente la parte de 3D. En este caso no ha sido tan fácil encontrar soporte a modo de
guías de desarrollo o personas que participen en foros a los que se pudieran plantear dudas. La
manera de ir aprendiendo ha sido a través de los ejemplos que trae el kit de desarrollo para
esta plataforma, devkitpro. El proceso ha consistido en ir analizando cada uno de los ejemplos
y a partir de estos ir sacando conclusiones. Aunque también han existido un par de ventajas
que han facilitado un poco el aprendizaje para desarrollo 3D en Nintendo DS. La primera ha
sido de la característica open source de la librería libnds que en un momento dado se puede
recurrir a ver directamente como implementa para acertar en un desarrollo adecuado, y la otra
ventaja es que la parte 3D es una adaptación de la conocida librería openGL. Esto último no
significa que se pueda utilizar toda la documentación existente sobre openGL para poder
realizar aplicaciones 3D en Nintendo DS, ya que es una versión bastante reducida, sin
embargo, sí me ha servido en ocasiones para orientarme.
Una de las preocupaciones que he tenido durante la realización del proyecto, desde el
planteamiento hasta el final del desarrollo, ha sido la idea de que llegado a un punto podría
topar con un obstáculo insalvable. Es cierto que las librerías que existen para esta plataforma
están bien hechas, muy documentadas y mucha gente realiza aplicaciones sobre estas. Pero de
nuevo es notable la poca gente que se ha dedicado al 3D con estas. Existía la posibilidad de
encontrar dificultades técnicas y así fue, sin embargo no fueron tan graves como para llegar a
hacer inviable la aplicación. Los inconvenientes que más tiempo invertí en resolver fueron
dos y ambos relacionados con la parte 3D. El primero fue cuando ya había creado la
aplicación para convertir modelos 3D a la estructura de C++ para openGL, al probar algunos
modelos creados sucedía que no se mostraban en la aplicación. Sin motivo aparente había
algunos modelos que se mostraban y otros no. Después de varios días buceando en Internet,
en un foro encontré a una persona que comentaba que existe una limitación en el número
decimal que se emplea en las coordenadas de los objetos 3D y que debía ser inferior a 7.0f. El
otro inconveniente fue al empezar a cargar las escenas con diferentes entidades, cada una con
una textura propia. Al cargar más de tres aparecían de color blanco. Del mismo modo acudí al
gran buscador en busca de respuestas. Tras un par de días investigando, en otro foro encontré
la solución. Existe otra limitación en la cantidad de memoria destinada a la carga de texturas
mientras que yo estaba creando todas a una resolución de 128x128 píxeles, se llenaba la
memoria con tres texturas. La solución fue usar todas las texturas de 64x64 excepto las más
complejas como la del usuario protagonista. De esta manera por cada textura de 128x128 se
podían usar cuatro de 64x64.
A parte de los inconvenientes, la tarea que más tiempo dediqué a pensar cómo resolver fue la
carga de figuras 3D. El resultado era la aplicación con interfaz gráfica que convierte un
fichero en formato X en otro binario. Sin embargo, para llegar hasta ese punto tuve que
investigar bastante y tomar varias decisiones. Como ya se ha comentado se utiliza una versión
simplificada de openGL. La única manera que disponía para pintar estructuras 3D en la
escena es a través de las primitivas para pintar triángulos y quads. Para pintar cubos no hay
problema, pero pintar objetos 3D modelados es otro asunto. Pasé un tiempo investigando,
preguntando e informándome si existía algún exportador de formatos pero no llegué a nada en
claro, ya que si lo había no cumplía con todas mis necesidades. La solución era implementarlo
desde cero. Tenía claro que los modelos los haría con alguna herramienta de modelado 3D
simple y gratuita, por ese entonces encontré un grupo de iniciación al modelado que usaban la
herramienta wings3D que me sirvió para aprender a manejarla. Ya había conseguido elaborar
los modelos, ahora necesitaba una estructura que pudiera parsear y convertir a otra estructura
que cumpliera con mis necesidades y pudiera emplear junto con las primitivas de openGL.
Uno de los inconvenientes siguientes fue decidir cuál estructura de entre todos los estándares
que existen podría valerme, realmente aquí el problema no era que no existieran posibilidades
factibles, al revés, existen muchísimas. Buscando en Internet guías para leer ficheros que
contenían figuras 3D, encontré varios enlaces que hacían referencia al formato usado por
DirectX decidiéndome por él en su exportación de texto, en lugar de datos binarios. También
tuve que encontrar la manera de obtener dicho formato a partir de wings3D. La solución
consistía en exportar el modelo desde wings3D a formato OBJ y éste llevarlo a 3D Studio
donde a través de un plugin se exporta a formato X. Por último fue necesario decidir qué
estructura definir para organizar la información de la figura 3D.
• Sonido
Esta aplicación consiste en un sistema multimedia y cuánto más atractivo resulte al usuario,
mejor podrá venderse el producto. Uno de los aspectos fundamentales que todos los juegos
incorporan es un sistema de sonido y efectos especiales. En esta aplicación no se ha podido
llevar a cabo pero no resultaría algo demasiado explicado. Está explicado en el apartado de la
librería PAlib cuales son las funciones responsables para esta utilidad. La idea sería que
durante la partida se escuchara una música y que según los eventos se reproduciesen
diferentes eventos. No estaría mal la idea de asociar efectos de sonido a las Entidades puesto
que por ejemplo una puerta puede chirriar al abrirse, o un objeto al caerse producir un ruido.
Del mismo modo cada objeto Escenario puede tener una música asociada de fondo.
• Guardar partidas
Esta aventura implementada es muy corta. Sin embargo, las aventuras que estamos
acostumbrados duran muchas horas de juego. Esto hace necesario un sistema para guardar los
progresos, acompañado de una interfaz que permita cargar la partida en un tiempo posterior.
Permitiendo al usuario apagar la consola quedando los datos residentes en una memoria no
volátil. Esto es posible gracias a la librería PAlib ya que ha incorporado funciones para
acceder al sistema de ficheros de la consola. Esto puede encontrarse en la sección PA File
System [29] de la API.
• Micrófono
La videoconsola Nintendo DS tiene además otros componentes hardware, quizás menos
convencionales en juegos, que pueden ser explotados y proporcionar experiencias muy
curiosas. Este es el caso del micrófono que trae incorporado y que también con la librería
PAlib permite ser accedido desde software [30].
Apéndice A. Glosario
• Homebrew
Término aplicado frecuentemente aplicado a los videojuegos que son desarrollados por
aficionados. Podría traducirse por software casero.
• Retrocompatibilidad
En el ámbito de las videoconsolas, capacidad de un sistema para poder cargar juegos
desarrollados para los sistemas predecesores a este.
• Flashear
Utilizado dentro del contexto de manipular la videoconsola Nintendo DS para permitir cargar
aplicaciones homebrew en ella, a través de la modificación del firmware original, con el
riesgo que esto supone si sucediera algún problema durante el proceso.
• Firmware
Programa que está incluido en un dispositivo hardware, o una memoria ROM; que puede ser
actualizado por el usuario. Este es ejecutado por el microprocesador del dispositivo. La
actualización del firmware suele ser realizada para corregir errores o añadir nuevas
funcionalidades al dispositivo.
• Open source
Se trata de un conjunto de artículos y prácticas sobre cómo escribir software, la más
importante de ellas es que el código fuente está disponible. La definición de Open Source fue
creada por Bruce Pernees para el proyecto Debian y actualmente es mantenida por la “Open
Source Initiative” [31] que añade un significado adicional al término: una persona no sólo
debería poder obtener el código fuente, sino además tener el derecho de usarlo.
• supercard, G6 o M3
Distintas compañías que han desarrollado sistemas de carga de backups que se introducen en
los diferentes slots de la videoconsola Nintendo DS.
• Frame
Se trata de un de las muchas imágenes fotográficas que componen el efecto visual de una
animación. Normalmente 24 frames son necesarios para un segundo de película.
• Tiles
Imagen pensada para ser utilizada junto a otras con el fin de formar una imagen de mayor
tamaño como si de un mosaico se tratara.
• Pad
También conocido como gamepad, joypad o pad de control; es un tipo de control para juegos
a través de las manos donde los dígitos son usados para presionar sobre los botones.
Generalmente traen un conjunto de botones de acción para manejarlos con el pulgar derecho y
un control de dirección, o cruceta, manejada con la mano izquierda.
• Sprite
Se trata de una imagen bidimiensinal que puede ser integrada en una escena de animación.
• Alpha-blending
Consiste en el efecto visual de permitir efectos de transparencia en los sprites. El valor de
alpha en el código de color puede tomar desde 0.0 a 1.0, donde el primer valor representa
completamente transparente, y el valor más alto indica un color totalmente opaco.
• Formato raw
Este tipo de formato es un concepto genérico que viene a indicar que los datos se han tomado
en ‘bruto’, es decir, sin tratamiento o sin procesar.
• mapeo UV
Es el proceso en el modelador 3D, para hacer que una imagen 2D envuelva o represente a un
modelo 3D.
• Memoria: Se guarda esta memoria en formato Word y Pdf, además de los códigos usados
en ésta.
• Códigos de la memoria: Aquí se pueden encontrar los códigos utilizados durante
algunos capítulos de la memoria.
Bibliografía
Enlaces
[24] wings3d
http://www.wings3d.es/
[25] Autodesk 3ds Max (inglés)
http://www.autodesk.es/3dsmax
[26] PandaSoft (inglés)
http://www.andytather.co.uk/Panda/directxmax.aspx
[27] Formato DirectX (inglés)
http://msdn.microsoft.com/archive/default.asp?url=/archive/en-
us/dx81_c/directx_cpp/Graphics/Reference/FileFormat/FileFormat.asp
[28] Microsoft Developer Network
http://msdn2.microsoft.com/es-es/default.aspx
[29] API para programar el micrófono de Nintendo DS (inglés)
http://palib.info/Doc/PAlibDoc%20Eng/group___micro.html
[30] API para programar el micrófono de Nintendo DS (inglés)
http://palib.info/Doc/PAlibDoc%20Eng/group___micro.html
[31] Open Source Initiative
http://opensource.org/
Librería libnds
http://devkitpro.sourceforge.net/devkitProWiki/libnds/
http://www.drunkencoders.com/documents/DS/ndslib.htm
http://www.elotrolado.net/showthread.php?s=&threadid=560011 (mención especial a la
persona tras el nick de webez por la excelencia de los artículos que alojó en este foro)
Librería PAlib
http://palib.com/
http://www.palib.info/Doc/PAlibDoc%20Eng/modules.html
http://www.palib.info/wiki/doku.php
PAlib
http://sourceforge.net/project/showfiles.php?group_id=142901&package_id=168612
http://www.palib.info/wiki/doku.php
http://www.talfi.net/xoops/modules/newbb/viewtopic.php?topic_id=50&forum=23
http://www.palib.info/Doc/PAlibDoc%20Eng/group___text.html
http://www.palib.info/wiki/doku.php?id=day4
• Capítulo 6. Implementación:
Programación 3D
http://www.console-dev.de/n3d.html
Formatos 3D
http://local.wasp.uwa.edu.au/~pbourke/dataformats/
Evan Pipho, “Focus on 3D Models”, The Premier Press (Game Development Series), 2003.
Formato Direct X
http://www.xbdev.net/3dformats/x/xfileformat.php
http://local.wasp.uwa.edu.au/~pbourke/dataformats/directx/
http://msdn.microsoft.com/archive/default.asp?url=/archive/en-
us/dx81_c/directx_cpp/Graphics/Reference/FileFormat/FileFormat.asp
http://www.andytather.co.uk/Panda/directxmax_downloads.aspx
Quateriones
http://msdn2.microsoft.com/es-
es/library/microsoft.windowsmobile.directx.quaternion(VS.80).aspx
Búsqueda de caminos
Mark A. DeLoura, “Game Programming Gems 1”, Charles River Media, 2000.
Stuart Jonathan Russell, Peter Norvig, “Artificial Intelligence: A Modern Approach” 2nd
Edition, Prentice Hall, 2003.