ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA INFORMÁTICA INGENIERO TÉCNIDO EN INFORMÁTICA DE SISTEMAS Desarrollo de Videojuego 3D Para La Videoconsola Nintendo DS Realizado por Fernando García Bernal Dirigido por Francisco R. Villatoro Machuca Departamento Lenguajes y Ciencias de la Computación UNIVERSIDAD DE MÁLAGA MÁLAGA, ABRIL 2008 UNIVERSIDAD DE MÁLAGA ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA INFORMÁTICA INGENIERO TÉCNICO EN INFORMÁTICA DE SISTEMAS Reunido el tribunal examinador en el día de la fecha, constituido por: Presidente Dº/Dª. Secretario Dº/Dª. Vocal Dº/Dª. para juzgar el proyecto Fin de Carrera titulado: Desarrollo de videojuego 3D para la videoconsola Nintendo DS del alumno D. Fernando García Bernal dirigido por Dr. D. Francisco R. Villatoro Machuca ACORDÓ POR _________________ OTORGAR LA CALIFICACIÓN DE ___________________ Y PARA QUE CONSTE, SE EXTIENDE FIRMADA POR LOS COMPARECIENTES DEL TRIBUNAL, LA PRESENTE DILIGENCIA. Málaga, a de del 2007 El Presidente El Secretario El Vocal Fdo.: Fdo.: Fdo.: Índice Capítulo 1. Introducción ...................................................................1 1.1 Antecedentes........................................................................................... 1 1.2 Objetivos ................................................................................................. 2 1.3 Contenidos de la memoria ..................................................................... 3 Capítulo 2. Videoconsola Nintendo DS ............................................5 2.1 2.1.1 2.2 Hardware................................................................................................ 6 Hardware adicional para cargar aplicaciones .....................................................6 Desarrollo en Nintendo DS .................................................................... 8 2.2.1 Libnds....................................................................................................................9 2.2.2 PAlib....................................................................................................................10 Capítulo 3. Librería PAlib ..............................................................13 3.1 Configuración del Entorno de Desarrollo y Emuladores................... 13 3.2 Programación en Nintendo DS Usando la Librería PAlib ................. 17 3.2.1 Plantilla PAlib .....................................................................................................17 3.2.2 Primera aplicación ..............................................................................................18 3.2.3 Textos ..................................................................................................................19 3.2.4 Entrada................................................................................................................21 3.2.4.1 Pad.......................................................................................................................21 3.2.4.2 Stylus ...................................................................................................................21 3.2.5 Imágenes..............................................................................................................22 3.2.5.1 Mostrando imágenes ...........................................................................................24 3.2.5.2 Desplazando imágenes ........................................................................................26 3.2.5.3 Rotaciones y zoom...............................................................................................29 3.2.5.4 Volteo de imágenes..............................................................................................33 3.2.5.5 Transparencias....................................................................................................34 3.2.5.6 Profundidad ........................................................................................................35 3.2.6 Funciones Matemáticas ......................................................................................36 3.2.6.1 Números aleatorios .............................................................................................36 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 Capítulo 4. Fase de Análisis del Juego ...........................................75 4.1 Idea........................................................................................................ 76 4.2 Guión .................................................................................................... 77 4.3 Modelado conceptual ........................................................................... 77 4.3.1 Fases del juego ....................................................................................................77 4.3.2 Elementos ............................................................................................................78 4.4 Diagrama de casos de uso .................................................................... 79 4.4.1 Diagrama de casos de uso del actor....................................................................79 4.4.2 Diagrama de casos de uso del usuario ................................................................79 Capítulo 5. Fase de Diseño ..............................................................81 5.1 Diseño de Personaje y Objetos............................................................. 81 5.2 Diseño de la Interfaz del Usuario ........................................................ 82 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 Capítulo 7. Conclusiones ...............................................................191 7.1 Inconvenientes y opiniones ................................................................ 191 7.2 Futuros desarrollos ............................................................................ 194 Apéndice A. Glosario.....................................................................197 Apéndice B. Contenido del CD-ROM ..........................................201 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. El desarrollo de los videojuegos se ha convertido en una tarea multidisciplinaria, que involucra profesionales de la informática, el diseño, el sonido, la actuación… Los estudios en informática sobre: inteligencia artificial, redes neuronales, modelado de procesos físicos, etc. tienen aplicación directa en el desarrollo de videojuegos. Recientemente están surgiendo en universidades españolas los primeros másters de diseño y desarrollo de videojuegos para completar la formación de los informáticos. Como por ejemplo “I Máster Universitario en Fernando Garcia Bernal 1 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Introducción Creación y Desarrollo de Videojuegos” de la Universidad de Málaga [1], “Máster en Desarrollo de Videojuegos” de la Universidad Complutense de Madrid [2] o el “Máster en Diseño y Programación de Videojuegos” de la Universidad Europea de Madrid [3]. 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 Fernando Garcia Bernal 2 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Introducción 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. 1.3 Contenidos de la memoria La presente memoria se divide en los siguientes capítulos: • Capítulo 2. Videoconsola Nintendo DS: Se explican los detalles técnicos que pueden ser de utilidad sobre la videoconsola sobre la que se va a desarrollar la aplicación objetivo de este proyecto. Dentro del capítulo se abarca los aspectos relacionados con el hardware y las librerías que existen para desarrollar aplicaciones. • Capítulo 3. Librería PAlib: Este apartado se centra en el desarrollo de aplicaciones para Nintendo DS usando una de las librerías más comunes. A lo largo del capítulo se explica cómo configurar el entorno dejándolo listo para el desarrollo, las secciones más importantes de la librería y, finalmente, se dedica al desarrollo de aplicaciones gráficas 3D. • Capítulo 4. Fases de Análisis del Juego: A partir de este capítulo, comienza a centrarse en la aplicación objetivo de esta memoria, empezando por la fase de análisis de requisitos que debe tener el juego. Se divide en la Fernando Garcia Bernal 3 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Introducción 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 5. Fase de Diseño: Tras analizar el proyecto en el capítulo anterior, en este se pasa a diseñar los elementos que formarán parte de juego, como son el personaje protagonista y los objetos con los que interactúa, la interfaz gráfica del usuario, y las clases que componen la aplicación. • 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. Fernando Garcia Bernal 4 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS Capítulo 2. Videoconsola Nintendo DS La videoconsola Nintendo DS es una consola portátil, desarrollada por Nintendo cuya primera aparición fue en 2004 [7]. La principal característica que la distingue del resto es que tiene dos pantallas LCD, siendo además la inferior táctil (figura 1). 1 - Foto de la consola NintendoDS mostrando sus dos pantallas y sus botones. 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. Fernando Garcia Bernal 5 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS 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. 2.1.1 Hardware adicional para cargar aplicaciones El objetivo del proyecto es desarrollar aplicaciones para la videoconsola Nintendo DS. Pero esto requiere o bien de un emulador para probar los códigos compilados, o bien probar la aplicación en el hardware destino final, la propia videoconsola. Esto último presenta un problema y es que para ejecutar los juegos oficiales basta con introducir el cartucho de juego original en su ranura correspondiente, pero para las aplicaciones que desarrollemos esto no es Fernando Garcia Bernal 6 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS 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): 2 - Foto de cartucho de Game Boy Advance al que se le puede introducir una tarjeta MiniSD con aplicaciones homebrew. 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): Fernando Garcia Bernal 7 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS 3 - Foto de cartucho de Nintendo DS para poder cargar aplicaciones desde la ranura de Game Boy Advance. 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): 4 - Foto de cartucho de Nintendo DS que permite la grabación directa de aplicaciones en la tarjeta de memoria microSD que se le incorpora. 2.2 Desarrollo en Nintendo DS En este capítulo se pretende dar una idea general de la situación actual del desarrollo para la videoconsola Nintendo DS. Fernando Garcia Bernal 8 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS 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 Fernando Garcia Bernal 9 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS 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 Fernando Garcia Bernal 10 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Videoconsola Nintendo DS 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. Fernando Garcia Bernal 11 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fernando Garcia Bernal Videoconsola Nintendo DS 12 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib Capítulo 3. Librería PAlib 3.1 Configuración del Entorno de Desarrollo y Emuladores Antes de comenzar a escribir programas con la ayuda de esta librería es necesario tener instalado y configurado una serie de herramientas. La primera utilidad a instalar es devkitpro [6] que se define como un conjunto de herramientas para desarrollar videojuegos homebrew. Actualmente da cobertura a las siguientes plataformas: PlayStation Portable (PSP), Game Cube, Game Boy Advance, GP32, y por supuesto Nintendo DS. Una vez descargado el fichero de instalación ejecutable desde su página web, se instala teniendo en cuenta que tan sólo es necesario las librerías que hacen referencia a Nintendo DS siendo éstas las que están bajo el menú devkitARM. En el momento de escribir esto, la última versión disponible de devkitpro es la 1.4.4 la cual incorpora el devkitARM release 20. 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]. Fernando Garcia Bernal 13 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. Para comprobar que se ha instalado correctamente, vamos a intentar compilar un código de ejemplo. Dentro de PAlibExamples\Text\Normal\HelloWorld arrancar el archivo por lotes build.bat si todo ha ido correcto se generan al menos tres archivos: HelloWorld.nds, HelloWorld.sc.nds y HelloWorld.ds.gba. Si ha sucedido algún error lo más probable esque haya que configurar las rutas. Considerando que el error es parecido a este: “make main.c arm-eabi-gcc.exe: CreatProcess: No such file or directory make[1]: * [main.o] Error 1 make: * [build] Error 2” entonces hay que indicar al sistema operativo dentro del path donde se encuentran los binarios que hacen falta para realizar la compilación. Para ello hay que aceder a Propiedades Avanzadas de Mi PC -> Variables de entorno ->Panel de Control y en las variables del sistema, modificar la variable Path (o crearla si no existe) para que incluya los siguientes directorios: “C: \devkitPro\ devkitARM\ bin; C:\ devkitPro \devkitARM \arm-eabi \bin; C:\ devkitPro \devkitARM \libexec \gcc \arm-eabi \4.1.1”. Suponiendo que devkitpro está instalado en el directorio raíz C:\devkitPro. Si ya se han conseguido generar los ficheros .nds, .sc.nds y .ds.gba es hora de arrancarlos. La opción más cómoda para el desarrollador es usar un emulador. Existen varios funcionales, entre los mejores está No$GBA [15] que tiene una versión comercial y otra gratuita pero con restricciones, además de otros como Dualis [9], iDeas [10], DSemu [8]. Algunos vienen incluidos con la instalación de devkitPro. Hay que decir que ningún emulador funciona al 100%. Aunque no he llegado a utilizar el emulador No$GBA, hablan muy bien de su rendimiento. Sin embargo, he comprobado que en algunos emuladores funcionan mejor unas características mientras que otras no tienen un buen resultado, por tanto lo mejor es tener instalado al menos dos emuladores distintos y por supuesto no fiarse de los resultados que estos puedan ofrecer teniendo que testear las aplicaciones en el hardware de la videoconsola NDS cuando sea posible para cerciorarse de que funcione correctamente. Si sirve de referencia el emulador que he usado como principal es iDeaS Emulator [10], aunque esto no Fernando Garcia Bernal 14 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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 Fernando Garcia Bernal 15 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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): 5 - PAlib configurado en Visual Studio. 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. Fernando Garcia Bernal 16 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib Lo siguiente es desarrollar aplicaciones que abarquen los aspectos más importantes de la librería para aprender a utilizarla. 3.2 Programación en Nintendo DS Usando la Librería PAlib 3.2.1 Plantilla PAlib Palib incluye una carpeta llamada PAlibTemplate que lleva dentro la estructura más básica pero completamente funcional de una aplicación para DS. Esta plantilla se compone de los siguientes ficheros: • makefile: fichero que compila el proyecto. No es necesario modificarlo aunque tiene variables internas por si hace falta. • clean.bat: limpia el directorio de los archivos generados por posibles compilaciones anteriores. • make.bat: primero realiza la acción clean y luego hace una compilación del proyecto. • logo.bmp y logo_wifi.bmp: se trata de dos imágenes que representarán los iconos de la aplicación cuando se vaya a transferir el archivo compilado vía wifi. El primero debe tener 32x32 pixeles de tamaño y una paleta de 16 colores, mientras que el segundo es de 104x16 colores y sólo puede tener dos colores: blanco y negro. • Template.pnproj: es el fichero de proyecto para el editor Programmers Notepad 2, si se abriera este fichero con dicho programa, se abriría con la estructura de ficheros del proyecto. • Project.vhw: Igual que Template.pnproj pero para el editor Virtual Ham. • Carpeta source: contiene el fichero main.c que será ejecutado como el inicio de la aplicación. En él viene un código que carga e inicializa la librería y que comentaremos más adelante. • Carpeta include: lugar donde se sitúan los ficheros de cabecera .h de C++. • Carpeta data: en esta carpeta se incluyen los recursos necesarios por la aplicación, como pueden ser imágenes, archivos de audio, modelos 3D o ficheros binarios en general. Fernando Garcia Bernal 17 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 3.2.2 Primera aplicación Una vez vista la plantilla, comenzaremos explicando su código para incluir más adelante alguna cosa más y terminar compilándolo y ejecutándolo. Este es el código de main.c: // 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 // Bucle infinito para mantener el programa en ejecución while (1) { PA_WaitForVBL(); } 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 Fernando Garcia Bernal referencia a algo comentado anteriormente. Se trata de 18 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib PA_WaitForVBL(), esta es la línea que hace sincronizar la aplicación a 60 fps, situándola al final del bucle se asegura que este se repetirá con la periodicidad indicada. Por último se realiza una llamada a return 0 para acabar correctamente con la aplicación, aunque recordemos que previamente existe un bucle infinito por tanto nunca se llegará hasta aquí a no ser que se salga explícitamente del bucle, con un break por ejemplo, lo cual no es nada deseable. 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"); // Bucle infinito para mantener el programa en ejecución while (1) { PA_WaitForVBL(); } 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 Fernando Garcia Bernal 19 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib función, PA_InitText (u8 screen, u8 bg_select) tiene dos parámetros. El primero es para indicar en cual pantalla se va a pintar (0 = inferior, 1 = superior), y el segundo hace referencia al fondo dentro de la pantalla donde se pintará (0-3). De momento basta con saber que NDS puede mostrar hasta cuatro fondos por cada pantalla, donde cada fondo tiene su propia paleta de 256 colores. Estos fondos pueden ser formados por tiles o bien de 8 o 16 bits de información. La otra función es PA_OutputSimpleText (u8 screen, u16 x, u16 y, const char *text). El primer parámetro representa lo mismo, la pantalla. Los dos siguientes son las coordenadas dentro de la pantalla, ‘x’ es la posición horizontal e ‘y’ es la vertical. La posición (0,0) se corresponde con la esquina superior izquierda y los valores positivos van hacia la derecha para la ‘x’ y hacia abajo para la ‘y’. La esquina inferior derecha tiene como coordenada (255,191) que se corresponden con las dimensiones de la pantalla en píxeles, 256x192. Sin embargo, para hacer referencia a las posiciones del texto debemos darnos cuenta de que el texto son imágenes cargadas en tiles, por tanto si un tile está formado por 8x8 píxeles, entonces la pantalla está formada por 256/8x192/8 = 32x24 tiles siendo (31,23) el último tile donde se podrá escribir texto en la pantalla. El último parámetro es el texto que se quiere mostrar. El código anterior se corresponde con la aplicación que muestra el texto “Hola Mundo DS”. 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. Fernando Garcia Bernal 20 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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: Fernando Garcia Bernal 21 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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: Fernando Garcia Bernal 22 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 6 - Distribución pantalla Nintendo DS. 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 Fernando Garcia Bernal 23 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. 3.2.5.1 Mostrando imágenes El primer ejercicio es uno de los que trae la librería PAlib para el que hará falta una imagen que trae en una de sus subcarpetas. El ejemplo está en PAlibExamples/Sprites/Basics/CreateSprite. El código del fichero main es: #include <PA9.h> // PAGfxConverter Include #include "gfx/all_gfx.h" #include "gfx/all_gfx.c" int main(void){ PA_Init(); //PAlib inicialización PA_InitVBL(); PA_LoadSpritePal( 0, 0, (void*)sprite0_Pal); PA_CreateSprite( 0, 0, (void*)vaisseau_Sprite, OBJ_SIZE_32X32, 1, Fernando Garcia Bernal // Pantalla // Número de paleta // Nombre de paleta // // // // // Pantalla Número de sprite Nombre de sprite Tamaño de sprite Modo de color 256 24 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS 0, 50, 50); Librería PAlib // Número de paleta de sprite // Posiciones X e Y en pantalla while(1) // Bucle infinito { PA_WaitForVBL(); } return 0; } 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. Fernando Garcia Bernal 25 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 3.2.5.2 Desplazando imágenes PA_MoveSprite El siguiente código seguirá sin tener demasiada complejidad pero se trata de una tarea fundamental para poder realizar aplicaciones interactivas como es el caso de los videojuegos o aplicaciones gráficas. // 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 la paleta de imágenes PA_LoadSpritePal( 0, // Pantalla 0, // Número de paleta (void*)sprite0_Pal); // Nombre de paleta // 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. Fernando Garcia Bernal 26 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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). Como nombre de referencia de cada sprite se le da el de la variable i, que como ya se ha dicho toma valores de 0 a 15, siendo este el que se usará para hacer mover cada sprite junto con el puntero. Se trata de una tarea sencilla haciendo uso de la función PA_MoveSprite (idsprite). Llamando a esta función e indicándole el id del sprite, este se desplazará si el puntero pasa por encima y se desvinculará cuando este se despegue. 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: //Mover un sprite usando los cursores #include <PA9.h> // PAGfx Include Fernando Garcia Bernal 27 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib #include "gfx/all_gfx.c" #include "gfx/all_gfx.h" s32 x = 0; s32 y = 0; // posición del sprite //Función main int main(void){ PA_Init(); //Inicialización de PAlib PA_InitVBL(); PA_InitText(0,0); PA_LoadSpritePal( 0, 0, (void*)sprite0_Pal); // Pantalla // Número de paleta // Nombre de paleta //Creación del sprite PA_CreateSprite( 0, 0,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 0, 0); while(1){ // Bucle principal // Actualiza la posición de acuerdo con la pulsación // del cursor x += Pad.Held.Right - Pad.Held.Left; y += Pad.Held.Down - Pad.Held.Up; // Establece la posición del sprite PA_SetSpriteXY( 0, // pantalla 0, // sprite x, // posición x y); // posición y PA_WaitForVBL(); } return 0; } Dentro del bucle principal se actualizan dos variables x e y. La línea x += Pad.Held.Right - Pad.Held.Left; incrementará la variable x en 1 si se ha pulsado hacia la derecha o se decrementará si se ha pulsado izquierda. En el caso de que ambos lados estuviesen pulsados la resta devolvería cero y se mantendría el valor de x. Lo mismo ocurre con la variable y pero atendiendo a las pulsaciones de arriba y abajo. Una vez que se conoce la posición x e y a la que debe desplazarse el sprite, se llama a la función PA_SetSpriteXY para moverlo. Si, por ejemplo, se quisiera desplazar la imagen al lugar donde se pulse con el puntero, habría que cambiar la línea Fernando Garcia Bernal PA_SetSpriteXY(0, 0, x, y); por esta 28 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS PA_SetSpriteXY(0, 0, Stylus.X, Librería PAlib Stylus.Y);. Ya que las variables Stylus.X y Stylus.Y almacenan la coordenada donde se encuentra pulsado el puntero. Lo siguiente será ver cómo se pueden aplicar transformaciones a las imágenes como por ejemplo rotaciones y zoom. 3.2.5.3 Rotaciones y zoom Como se dijo anteriormente, los sprites pueden ser rotados y/o escalados pero existe una pequeña limitación: todos los sprites pueden ser rotados sin embargo un sprite rotado necesita que se le aplique una rotación previamente definida llamada rotset y se pueden establecer hasta 32 rotsets diferentes por pantalla pudiendo aplicar un mismo rotset a diferentes sprites que presentarán la misma deformación. A continuación tres ejemplos: rotación, escalado y rotación/escalado. Rotación Este es el código del ejemplo: // Activa rotación de sprite y lo hace girar #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, 0, (void*)sprite0_Pal); // Pantalla // Número de paleta // Nombre de paleta // Carga el sprite PA_CreateSprite( 0, 0,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 50, 50); // Activa las rotaciones para el sprite PA_SetSpriteRotEnable( 0,// pantalla 0,// Número de sprite 0);// Número de rotset. Hay 32 (0-31) por pantalla Fernando Garcia Bernal 29 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib u16 angle = 0; // Ángulo de rotación... 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; // Función para rotaciones sin zoom PA_SetRotsetNoZoom( 0, //pantalla 0, // rotset angle); // ángulo, de 0 a 511 PA_WaitForVBL(); // Sincronización } return 0; } La primera función nueva que aparece es PA_SetSpriteRotEnable(screen, sprite, rotset). Con esta función se vincula una sprite existente en una pantalla con un rotset. A partir de esta llamada el sprite está listo para ser transformado, si se quisiera deshabilitar esta opción habría que usarse la función PA_SetSpriteRotDisable(screen, sprite). Después se calcula el valor del ángulo de rotación que se le aplicará al sprite. Como se puede ver no se trata de un rango de ángulos normal de 360 grados sino que la vuelta completa cubre las posiciones que van de 0 a 511. La razón es porque de este modo es mucho más eficiente la Nintendo DS que con los ángulos normales. También indicar que el giro se produce en sentido contrario a las agujas del reloj. 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. Por último, se realiza la transformación haciendo uso de la función PA_SetRotsetNoZoom (u8 screen, u8 rotset, s16 angle). De este modo se modifica el rotset, como se puede apreciar no se hace referencia al id del sprite ya que este Fernando Garcia Bernal 30 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib se vinculó anteriormente y al modificar el rotset ya se aplica la transformación al sprite. Hay que indicar en cual pantalla se encuentra el rotset, su id (0-31) y el ángulo de giro (0-511). El giro del sprite se realiza desde su punto central. Zoom Código que muestra imágenes rotando: // Activa zoom de sprite #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, 0, (void*)sprite0_Pal); // Pantalla // Número de paleta // Nombre de paleta // Carga el sprite PA_CreateSprite( 0, 0,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 50, 50); // Habilita doble tamaño, lo cual significa que el sprite puede // ser más grande que su tamaño normal PA_SetSpriteDblsize(0, 0, 1); // Este es otro sprite sin doble tamañao para mostrar la diferencia PA_CreateSprite( 0, 1,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 120, 66); // Activa las rotaciones para el sprite PA_SetSpriteRotEnable( 0,// pantalla 0,// Número de sprite 0);// Número de rotset. Hay 32 (0-31) por pantalla // Mismo rotset para el otro sprite, se le aplicacrá la misma // transformación PA_SetSpriteRotEnable(0, 1, 0); // Zoom. 256 significa no zoom, 512 es el doble de pequeño, // 128 es el doble de grande u16 zoom = 256; while(1) { // Modifica el ángulo según las teclas Fernando Garcia Bernal 31 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib zoom -= Pad.Held.Up - Pad.Held.Down; // Función para zoom sin rotaciones PA_SetRotsetNoAngle( 0, // pantalla 0, // rotset zoom, zoom); // Horizontal zoom, Vertical zoom 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. Lo siguiente es aplicar la transformación, como ocurría en el ejemplo anterior esto es modificando las propiedades del rotset que está vinculado al sprite. En este caso usaremos una función que permite modificar exclusivamente el zoom, obviando la transformación de rotación. Esta función es PA_SetRotsetNoAngle (u8 screen, u8 rotset, u16 zoomx, u16 zoomy) que permite aplicar un zoom horizontal y otro vertical por separado. En este código se ha aplicado un mismo rotset a dos imágenes y como se puede observar, con hacer una sola llamada a esta función se modifica el rotset que a su vez se observa como el zoom se aplica a ambas imágenes. 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. Fernando Garcia Bernal 32 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. 3.2.5.4 Volteo de imágenes Se trata de conseguir que una imagen se vea reflejada bien horizontalmente o bien verticalmente. Las funciones para conseguir son PA_SetSpriteHflip(screen, obj, hflip) y PA_SetSpriteVflip(screen, obj, vflip) respectivamente. Lo único que hay que hacer es indicar en el tercer parámetro con 0 ó 1 si se quiere realizar la acción. El ejemplo que ilustra cómo hacer esta funcionalidad es el siguiente: #include <PA9.h> // PAGfxConverter Include #include "gfx/all_gfx.c" #include "gfx/all_gfx.h" int main(void){ PA_Init(); //Inicialización de PAlib PA_InitVBL(); PA_LoadSpritePal( 0, // Pantalla 0, // Número de paleta (void*)sprite0_Pal); // Nombre de paleta // Se crearán cuatro sprites que serán volteados // cada uno de diferente manera PA_CreateSprite( 0, 0, (void*)mollusk_Sprite, OBJ_SIZE_32X32, PA_CreateSprite( 0, 1, (void*)mollusk_Sprite, OBJ_SIZE_32X32, PA_CreateSprite( 0, 2, (void*)mollusk_Sprite, OBJ_SIZE_32X32, PA_CreateSprite( 0, 3, (void*)mollusk_Sprite, OBJ_SIZE_32X32, 1, 0, 0, 50); 1, 0, 64, 50); 1, 0, 128, 50); 1, 0, 192, 50); // Flip para los sprites 1 a 3 PA_SetSpriteHflip(0, 1, 1); // HFlip -> Horizontal flip PA_SetSpriteVflip(0, 2, 1); // VFlip -> Vertical flip // Horizontal y Vertical flip Fernando Garcia Bernal 33 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib PA_SetSpriteHflip(0, 3, 1); PA_SetSpriteVflip(0, 3, 1); while(1) // Bucle infinito { } 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_Init(); //Inicialización de PAlib PA_InitVBL(); Fernando Garcia Bernal 34 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib PA_LoadSpritePal( 0, // Pantalla 0, // Número de paleta (void*)sprite0_Pal); // Nombre de paleta // Se crearán dos sprites PA_CreateSprite( 0, 0, (void*)mollusk_Sprite, OBJ_SIZE_32X32, 1, 0, 0, 50); PA_CreateSprite( 0, 1, (void*)mollusk_Sprite, OBJ_SIZE_32X32, 1, 0, 64, 50); PA_SetSpriteMode( 0, // Pantalla 0, // Sprite 1); // Alphablending s16 alpha = 7; // Nivel de transparencia // 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 while(1) // Bucle infinito { alpha += Pad.Newpress.Up - Pad.Newpress.Down; PA_SetSFXAlpha( 0, // Pantalla alpha, // Nivel de alpha, 0-15 15); // Dejar a 15 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. Fernando Garcia Bernal 35 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib Para establecer prioridades se usa la función PA_SetSpritePrio(screen, obj, prio). 3.2.6 Funciones Matemáticas 3.2.6.1 Números aleatorios Los números aleatorios hacen falta en muchas ocasiones ya que permiten conseguir comportamientos que parecen no definidos, por ejemplo, son muy útiles en algoritmos de inteligencia artificial. A continuación se explican las funciones que generan estos números aleatorios. 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. Fernando Garcia Bernal 36 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 3.2.6.2 Decimales con punto fijo Este apartado explica un punto importante en relación con el rendimiento. Algo que siempre penaliza mucho las operaciones realizadas por el procesador son los cálculos con números decimales. Esto puede tener mayor repercusión incluso en sistemas con potencia limitada como es el caso de una videoconsola portátil. La DS es muy lenta trabajando con tipos de coma flotante como float. Sin embargo, evitar usarlos no es una solución en muchas ocasiones que es necesario una dimensión real. 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. A continuación se muestra un ejemplo de cómo desplazar imágenes a velocidades inferiores a la unidad. Se trata de una aplicación que mostrará 3 sprites alineados verticalmente donde cada uno se moverá con las velocidades de 1, 0.5 y 0.25 respectivamente. El código es el siguiente: #include <PA9.h> // PAGfxConverter Include #include "gfx/all_gfx.c" #include "gfx/all_gfx.h" int main(void){ PA_Init(); //Inicialización PAlib PA_InitVBL(); PA_InitText(0, 0); s32 speed1 = 256; // Velocidad 1 PA_OutputText(0, 15, 2, " 1 pixel/frame"); s32 speed2 = 128; // Velocidad 0.5 PA_OutputText(0, 15, 10, "0.5 pixel/frame"); s32 speed3 = 64; // Velocidad 0.25 PA_OutputText(0, 15, 18, "0.25 pixel/frame"); PA_LoadSpritePal(0, 0, (void*)sprite0_Pal); Fernando Garcia Bernal 37 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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); // Todos las imágenes comienzan a la izquierda s32 spritex1 = 0; s32 spritex2 = 0; s32 spritex3 = 0; while(1) // Bucle infinito { // Cada imagen se mueve con su velocidad correspondiente spritex1 += speed1; spritex2 += speed2; spritex3 += speed3; // Posiciona todos los sprites, >>8 lo devuelve a // su posición normal PA_SetSpriteX(0, 0, spritex1>>8); PA_SetSpriteX(0, 1, spritex2>>8); PA_SetSpriteX(0, 2, spritex3>>8); 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. 3.2.6.3 Trayectorias y ángulos En esta sección se va a explicar cómo se usan los ángulos en PAlib, como aplicar funciones trigonométricas como senos y cosenos, y después se verá un ejemplo. Fernando Garcia Bernal 38 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib Á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. 7 - Ángulos con PAlib. 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). El código para probar esta función es el siguiente: // Includes #include <PA9.h> #include "gfx/vaisseau.raw.c" #include "gfx/master.pal.c" int main(void){ PA_Init(); PA_InitVBL(); PA_InitText(1,0); // Pantalla superior PA_LoadSpritePal(0, 0, (void*)master_Palette); Fernando Garcia Bernal 39 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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: 8 - Seno y coseno con PAlib. Fernando Garcia Bernal 40 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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_InitText(1,0); // Pantalla superior PA_LoadSpritePal(0, 0, (void*)sprite0_Pal); // Crea la nave en el centro PA_CreateSprite( 0, 0,(void*)vaisseau_Sprite, OBJ_SIZE_32X32,1, 0, 128-16, 96-16); // Habilita rotaciones y establece el Rotset 0 PA_SetSpriteRotEnable(0,0,0); // Posición x de la nave en 8bit con punto fijo s32 x = (128-16) << 8; // Posición Y s32 y = (96-16) << 8; // Dirección a la que mover u16 angle = 0; while(1) { angle += Pad.Held.Left - Pad.Held.Right; // Gira la nave en la dirección correcta PA_SetRotsetNoZoom(0, 0, angle); if (Pad.Held.Up){ // Mueve hacia delante x += PA_Cos(angle); y -= PA_Sin(angle); } if (Pad.Held.Down){ // Mueve hacia atrás x += -PA_Cos(angle); y -= -PA_Sin(angle); } PA_OutputText(1, 5, 10, "Angulo : %d Fernando Garcia Bernal ", angle); 41 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib // Posición de imagen convertida a normal PA_SetSpriteXY(0, 0, x>>8, y>>8); 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. Dentro del bucle lo primero que se actualiza es el ángulo, increméntandose si se gira a la izquierda o decreméntandose si es a la derecha (sentido inverso a las agujas del reloj). Después se actualiza la transformación del rotset para girar el sprite. Por último es necesario actualizar las coordenadas de posición. A la posición x se le añade el coseno del ángulo, mientras que a la posición y es necesario restarle el seno del ángulo ya que la parte superior de la pantalla tiene y=0 lo contrario a los ejes de coordenadas habituales que las posiciones bajas de y están en la parte inferior, por eso es necesario restar. Finalmente se actualiza la posición con la función PA_SetSpriteXY teniendo en cuenta usar >> 8 para contrarrestar la operación << 8 usada en la inicialización de las variables. 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. Fernando Garcia Bernal 42 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. 3.2.7.1 Archivos de sonido Raw La DS no puede reproducir archivos wav ni otros formatos conocidos como mp3 u ogg, hace falta convertir los audios a un formato raw para luego reproducirlos. 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: 9 - Ejemplo exportar a .raw. 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. Fernando Garcia Bernal 43 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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: // Ejemplo de sonido. Formato Raw // Includes #include <PA9.h> #include "saberoff.h" // Include para PA_Lib // 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); PA_InitSound(); // Inicializa el sistema de sonido PA_OutputSimpleText(0, 0, 10, "Pulsa A para reproduc. el sonido"); // 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. Fernando Garcia Bernal 44 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 3.2.7.2 Archivos de sonido mod Los archivos mod tienen como ventaja que ocupan muy poco espacio y suenan bien. Sin embargo, no son fáciles de crear y por tanto se recomienda buscar en la web los archivos ya existentes en bancos de sonidos gratuitos como por ejemplo: The Mod Archive, Exotica , and ModLand. Ahora mediante un ejemplo se verá como reproducir estos archivos: // Reproduce un archivo mod // Includes #include <PA9.h> #include "modfile.h" // Include para PA_Lib // 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_InitSound(); // Inicializa el sonido PA_InitText(0, 0); PA_InitText(1, 0); PA_PlayMod(modfile); // Reproduce el archivo mod PA_OutputText(0, 5, 10, "Reproduciendo mod..."); u8 i; while(1){ // Bucle infinito // Muestra si los cancales están ocupados for (i = 0; i < 16; i++) PA_OutputText(1, 0, i, "Canal %02d ocupado : %d ", i, PA_SoundChannelIsBusy(i)); // 0 libre, 1 ocupado 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. Fernando Garcia Bernal 45 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 3.2.7.3 Funciones adicionales de sonido Hasta ahora se han visto las funciones básicas para reproducir los dos tipos de formatos que permite PAlib, sin embargo existen más funcionalidades a parte de reproducir sonidos. Las más importantes son las siguientes. Para archivos mod existe la posibilidad de detener la reproducción con la función PA_StopMod() y también es posible pausarlo con PA_PauseMod(u8). Una función útil para archivos raw es la siguiente: PA_SetDefaultSound (u8 volume, int freq, s16 format), que establece las opciones del archivo como son volumen, frequencia y formato. Además el volumen se puede modificar a nivel de canal con PA_SetSoundChannelVol (u8 PA_Channel, u8 Volume) y también es posible controlar el master del volumen con PA_SetSoundVol (u8 Volume). 3.2.8 Programación Hardware DS En este apartado se estudia las diferentes funciones que aporta PAlib para trabajar con el hardware de DS como por ejemplo apagar la consola, información del usuario, reloj… 3.2.8.1 Fecha y hora Para poder gestionar en las aplicaciones diferentes eventualidades a lo largo del tiempo será necesario conocer las siguientes variables que harán uso del reloj interno que trae la consola y que debe estar previamente configurado por el usuario de la videoconsola. Con este sencillo ejemplo se accede a todo el contenido necesario: // Muestra la fecha y hora #include <PA9.h> // Función Main int main(void) { // Inicialización de PAlib PA_Init(); PA_InitVBL(); Fernando Garcia Bernal 46 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib // 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. 3.2.8.2 Información de usuario La información de usuario se encuentra almacenada en la variable PA_UserInfo. A continuación un ejemplo que la muestra por pantalla: // Muestra la información del usuario #include <PA9.h> // Función Main int main(void) { // Inicialización de PAlib PA_Init(); PA_InitVBL(); // Inicializa el texto PA_InitText(1, 0); Fernando Garcia Bernal 47 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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); // Idioma DS (0 - japonés, 1 - inglés, // 2 - francés .. 5 - español) PA_OutputText(1, 2, 14, "Idioma : %d", PA_UserInfo.Language); // Mensaje especial PA_OutputText(1, 2, 16, "Mensaje : %s", PA_UserInfo.Message); PA_WaitForVBL(); } return 0; } El contenido de la estructura PA_UserInfo es el siguiente: • PA_UserInfo.Name, nombre de usuario. • PA_UserInfo.BdayDay, día de cumpleaños. • PA_UserInfo.BdayMonth, mes de cumpleaños. • PA_UserInfo.Language, idioma identificado por un número (0 - japonés, 1 - inglés, 2 – francés, 3 – Alemán, 4 – Italiano, 5 - español). • PA_UserInfo.Message, mensaje de bienvenida establecido por el propietario. • PA_UserInfo.AlarmHour, hora de la alarma (de 0 a 23). • PA_UserInfo.AlarmMinute, minuto de la alarma. • PA_UserInfo.Color, color elegido por el propietario. 3.2.8.3 Pausa al cerrar En los juegos comerciales de se puede observar como al plegar la videoconsola cerrando las tapas, el juego se pausa automáticamente hasta que se vuelve a abrir. Esto se puede realizar también usando PAlib llamando simplemente a la función PA_CheckLid (void) que en Fernando Garcia Bernal 48 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib caso de detectar que se han cerrado las tapas, pausa la consola y devuelve un 1. Aquí se puede ver un ejemplo: // Muestra la información del usuario #include <PA9.h> // Función Main int main(void) { // Inicialización de PAlib PA_Init(); PA_InitVBL(); // Establece un fondo de color rojo para poder comprobar // si está pausada o no fácilmente PA_SetBgPalCol(0, 0, PA_RGB(31, 0, 0)); while (1) { PA_CheckLid(); // Comprueba si está cerrada PA_WaitForVBL(); } return 0; } Para probar este ejemplo es necesario disponer de una consola DS. 3.2.8.4 Iluminación de pantallas Con la función void PA_SetScreenLight (u8 screen, u8 light) se puede activar/desactivar cualquiera de las dos pantallas. 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… Fernando Garcia Bernal 49 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib El soporte para programación gráfica 3D lo ofrece una implementación de openGL dentro de la librería libnds. PAlib también incluye un módulo de funciones 3D que facilitan las tareas de libnds aunque se centra en el tratamiento de sprites 3D. Por tanto, y como el objetivo final de este proyecto consistirá en mostrar un escenario 3D no será tan necesario hacer uso del módulo 3D incluido dentro de PAlib. 3.3.1 Inicializar funciones 3D Hasta ahora la librería PAlib que se ha estudiado era la responsable de llamar a las funciones de más bajo nivel que componen la otra librería libnds, simplificando significativamente la tarea. Sin embargo, como se ha explicado anteriormente, el módulo de PAlib para la programación gráfica 3D no llega a sustituir completamente a la librería libnds. Por tanto, a partir de este momento será necesario combinar ambas librerías para poder simplificar las tareas lo máximo posible. El siguiente ejemplo no muestra nada por la pantalla, simplemente inicializa todo lo necesario para que después se puedan pintar componentes 3D correctamente. // Ejemplo inicializar 3D #include <PA9.h> //Función encargada de pintar la escena int DrawGLScene(); // Función Main int main(void) { // Inicialización de PAlib PA_Init(); PA_InitVBL(); //Inicialización 3D con PAlib PA_Init3D(); // Habilita antialias glEnable(GL_ANTIALIAS); // Activa matriz de proyección para modificar glMatrixMode(GL_PROJECTION); // Carga la matriz identidad glLoadIdentity(); // Pone la proyección en perspectiva ajustando // al tamaño de pantalla de NDS gluPerspective(35, 256.0 / 192.0, 0.1, 100); // Orienta la cámara Fernando Garcia Bernal 50 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS gluLookAt( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); Librería PAlib // Posición de la cámara // Hacia donde apunta // Vector up while (1) { // Push de la matriz activa (salva el estado) glPushMatrix(); DrawGLScene(); // Pop de la matriz actual (carga el estado salvado en la pila) glPopMatrix(1); PA_WaitForVBL(); // Limpia el buffer de la pantalla glFlush(0); } 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 Fernando Garcia Bernal 51 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib las transformaciones aplicadas a los objetos de la escena. La siguiente instrucción es glLoadIdentity(), cuya tarea es cargar la matriz identidad sobre la matriz establecida con glMatrixMode. De este modo se inicializa el contenido de la matriz eliminando todas las transformaciones que se hubieran aplicado a esta. Estando activada la matriz de proyección, es necesario cargar sobre esta el tipo de proyección que se quiere mostrar en la escena. Para ello se llama a la función gluPerspective(35, 256.0/192.0, 0.1, 100); cuyos parámetros significan respectivamente: 35 grados de ángulo de visión de altura (eje Y), relación de aspecto acho/alto, 256/192, que determina el ángulo de visión a lo largo del eje X, distancia desde el observador al plano de corte cercano, distancia desde el observador al plano de corte lejano. Lo siguiente es posicionar y orientar la cámara correctamente con gluLookAt. Sus parámetros se dividen en tres ternas: la primera establece la coordenada donde estará situada la cámara, la segunda el punto del escenario 3D hacia el que apunta la cámara, la última terna expresa el vector alzado de la cámara que forma 90º con el vector que se construye entre la posición hacia donde mira y la posición donde se encuentra. Dentro del bucle principal, se llama a glPushMatrix que guarda la matriz activada en lo alto de una pila. Con esto se salva su estado para poder recuperarlo más adelante desde la pila. Después se llama a la función que pintará la escena. Finalmente se recupera el estado de la matriz de transformaciones de modelos con glPopMatrix y se limpia el buffer de memoria con glFlush. 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: // Aquí es donde se pinta todo int DrawGLScene() { // Activa la matriz de modelos de escena glMatrixMode(GL_MODELVIEW); // Inicializa la matriz activa glLoadIdentity(); // Mueve 1.5 unidades a la izquierda y 6.0 hacia la pantalla Fernando Garcia Bernal 52 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib glTranslatef(-1.5f,0.0f,-3.0f); // Formato de polígonos, establece alpha a 31 (desactivado // transparencias) y el modo de procesado de // superficies ocultas a none glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE); // Dibuja triángulos glBegin(GL_TRIANGLES); glColor3f(1.0f,0.0f,0.0f); // // // // // // // // // // glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f(-1.0f,-1.0f, 0.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f( 1.0f,-1.0f, 0.0f); // Fin Triángulo glEnd(); Establece el color de vértice a Rojo Vértice superior Establece el color de vértice a Verde Vértice inferior izquierdo Establece el color de vértice a Azul Vértice inferior derecho // Desplaza 3 unidades a la derecha glTranslatef(3.0f,0.0f,0.0f); // Establece el color para los siguientes vértices a azul glColor3f(0.5f,0.5f,1.0f); // Dibuja un Quad glBegin(GL_QUADS); glVertex3f(-1.0f, 1.0f, glVertex3f( 1.0f, 1.0f, glVertex3f( 1.0f,-1.0f, glVertex3f(-1.0f,-1.0f, // Fin Quad glEnd(); 0.0f); 0.0f); 0.0f); 0.0f); // // // // Vértice Vértice Vértice Vértice superior superior inferior inferior izquierdo derecho derecho izquierdo 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 Fernando Garcia Bernal 53 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. Con la función glPolyFmt se establecen las propiedades asociadas a los polígonos de la escena. Para este caso se deja el valor de alpha (transparencia) a 31, sin transparencia, y se establece que ambos lados de los polígonos sean renderizados. Después comienza el pintado de un triángulo con glBegin(GL_TRIANGLES). Desde esta línea hasta glEnd() todos los pares de 3 vértices que se pinten formarán triángulos. Existen varios tipos de figuras simples que se pueden pintar. Para esta implementación de openGL son las siguientes: GL_TRIANGLES, GL_QUADS, GL_TRIANGLE_STRIP y GL_QUAD_STRIP. La primera es para dibujar triángulos, la segunda forma un cuadrado por cada grupo de 4 vértices, y las dos últimas son similares a las dos primeras salvo que cada grupo de vértice se encadenará al siguiente. En este ejemplo, dentro de los bloque glBegin y glEnd aparecen dos funciones distintas. La más significativa es void glVertex3f(float x, float y, float z) que pinta un vértice de la figura en las coordenadas x, y, z indicadas. La otra función es void glColor3f(float r, float g, float b) que establece el color a los vértices que se pinten a continuación según la terna rojo, verde y azul. En el ejemplo a cada vértice del triángulo se le aplica un color diferente, mientras que el cuadrado está pintado con todos los vértices del mismo color azul. 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. float rtri; float rquad; // Ángulo para el triángulo // Ángulo para el cuadrado int DrawGLScene() // Aquí es donde se pinta todo Fernando Garcia Bernal 54 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib { // Activa la matriz de modelos de escena glMatrixMode(GL_MODELVIEW); // Inicializa la matriz activa glLoadIdentity(); // Mueve 1.5 unidades a la izquierda y 6.0 hacia la pantalla glTranslatef(-1.5f,0.0f,-4.0f); // Gira el triángulo en el eje Y glRotatef(rtri,0.0f,1.0f,0.0f); // Formato de polígonos, establece alpha a 31 (desactivado // transparencias) y el modo de procesado de // superficies ocultas a none glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE); // Dibuja triángulos glBegin(GL_TRIANGLES); glColor3f(1.0f,0.0f,0.0f); glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f(-1.0f,-1.0f, 0.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f( 1.0f,-1.0f, 0.0f); // Fin Triángulo glEnd(); // // // // // // // // // // Establece el color de vértice a Rojo Vértice superior Establece el color de vértice a Verde Vértice inferior izquierdo Establece el color de vértice a Azul Vértice inferior derecho // Reinicia la actual matriz Modelview glLoadIdentity(); // Mueve 1.5 unidades a la derecha y 6.0 hacia la pantalla glTranslatef(1.5f,0.0f,-4.0f); // Gira el cuadrado en el eje X glRotatef(rquad,1.0f,0.0f,0.0f); // Establece el color para los siguientes vértices a azul glColor3f(0.5f,0.5f,1.0f); // 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(); // Aumenta la rotación variable para el triángulo rtri+=0.9f; // Decrementa la rotación variable para el triángulo rquad-=0.75f; Fernando Garcia Bernal 55 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib return TRUE; } La función que aparece nueva esta ocasión es void glRotatef(float angle, float x, float y, float z). Se encarga de transformar la escena con una rotación a todos los elementos que se pinten a continuación, Su primer parámetro es el ángulo de rotación y los tres siguientes son los ejes sobre los que se aplica. En esta aplicación se realiza una rotación diferente a cada polígono. Al comienzo de la función se inicializa la matriz olvidando transformaciones previas y se aplica una traslación como sucedía en el ejemplo anterior. Después se aplica la rotación sobre el eje Y. Se pinta el triángulo. Lo siguiente es Inicializar de nuevo la matriz modelview, ¿por qué? Es importante comprenderlo. El objetivo de este ejemplo es aplicar dos rotaciones diferentes e independientes entre sí a cada polígono. La primera rotación sobre el eje Y, y la segunda sobre el eje X. Si se aplicara la translación y la rotación sin más sucedería que se acumularían ambas rotaciones ya que sólo existe una matriz de escena modelview que es común a todos, por tanto es necesario volverla a inicializar para que las aplicaciones previas no se acumulen con las siguientes. Por tanto, a partir de su nueva inicialización será necesario volver a trasladar con respecto a la coordenada (0,0) y aplicar la rotación sobre el objeto. Es interesante comentar la línea de la segunda inicialización de la matriz para ver qué sucede en tal caso. Por último se actualizan los valores de las variables globales para conseguir el efecto de girar. 3.3.4 Figuras Básicas 3D Este apartado no añade ninguna función nueva sin embargo resulta necesario para comprender el nivel de abstracción con el que trabaja la librería de programación gráfica openGL. 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. Fernando Garcia Bernal 56 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. // Aquí es donde se pinta todo int DrawGLScene() { // Activa la matriz de modelos de escena glMatrixMode(GL_MODELVIEW); // Inicializa la matriz activa glLoadIdentity(); // Mueve 1.5 unidades a la izquierda y 6.0 hacia la pantalla glTranslatef(-1.5f,0.0f,-6.0f); // Gira el triángulo en el eje Y glRotatef(rtri,0.0f,1.0f,0.0f); // Formato de polígonos, establece alpha a 31 (desactivado // transparencias) y el modo de procesado de // superficies ocultas a none glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE); // Dibuja triángulos glBegin(GL_TRIANGLES); glColor3f(1.0f,0.0f,0.0f); glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f(-1.0f,-1.0f, 1.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f( 1.0f,-1.0f, 1.0f); glColor3f(1.0f,0.0f,0.0f); glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f( 1.0f,-1.0f, 1.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f( 1.0f,-1.0f, -1.0f); glColor3f(1.0f,0.0f,0.0f); glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f( 1.0f,-1.0f, -1.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f(-1.0f,-1.0f, -1.0f); glColor3f(1.0f,0.0f,0.0f); glVertex3f( 0.0f, 1.0f, 0.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f(-1.0f,-1.0f,-1.0f); Fernando Garcia Bernal // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // Rojo Vértice superior (Frente) Verde Vértice inferior izquierdo (Frente) Azul Vértice inferior derecho (Frente) Rojo Vértice superior (Derecha) Azul Vértice inferior izquierdo (Derecha) Verde Vértice inferior derecho (Derecha) Rojo Vértice superior (Atrás) Verde Vértice inferior izquierdo (Atrás) Azul Vértice inferior derecho (Atrás) Rojo Vértice superior (Izquierda) Azul Vértice inferior izquierdo (Izquierda) 57 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS glColor3f(0.0f,1.0f,0.0f); glVertex3f(-1.0f,-1.0f, 1.0f); Librería PAlib // Verde // Vértice inferior derecho // (Izquierda) // Fin Triángulo glEnd(); // Reinicia la actual matriz Modelview glLoadIdentity(); // Mueve 1.5 unidades a la derecha y 6.0 hacia la pantalla glTranslatef(1.5f,0.0f,-6.0f); // Gira el cuadrado en el eje X glRotatef(rquad,1.0f,1.0f,1.0f); // Dibuja un Quad glBegin(GL_QUADS); glColor3f(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); glColor3f(1.0f,0.5f,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); glColor3f(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); glColor3f(1.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); glColor3f(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); Fernando Garcia Bernal // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // Verde Vértice superior derecho (Arriba) Vértice superior izquierdo (Arriba) Vértice inferior izquierdo (Arriba) Vértice inferior derecho (Arriba) Color naranja Vértice superior derecho (Abajo) Vértice superior izquierdo (Abajo) Vértice inferior izquierdo (Abajo) Vértice inferior derecho (Abajo) Rojo Vértice superior derecho (Frente) Vértice superior izquierdo (Frente) Vértice inferior izquierdo (Frente) Vértice inferior derecho (Frente) Amarillo Vértice superior derecho (Atrás) Vértice superior izquierdo (Atrás) Vértice inferior izquierdo (Atrás) Vértice inferior derecho (Atrás) Azul Vértice superior derecho (Izquierda) Vértice superior izquierdo (Izquierda) Vértice inferior izquierdo (Izquierda) 58 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS glVertex3f(-1.0f,-1.0f, 1.0f); glColor3f(1.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); // // // // // // // // // // // Librería PAlib Vértice inferior derecho (Izquierda) Violeta Vértice superior derecho (Derecha) Vértice superior izquierdo (Derecha) Vértice inferior izquierdo (Derecha) Vértice inferior derecho (Derecha) // Fin Quad glEnd(); // Aumenta la rotación variable para el triángulo rtri+=0.9f; // Decrementa la rotación variable para el triángulo rquad-=0.75f; 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. Fernando Garcia Bernal 59 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 10 - Textura para el cubo. // Texturas #include <PA9.h> #include "Crate.h" // Textura a cargar float rtri; float rquad; int textureID; // variable id de la textura int DrawGLScene(); int main(void) { PA_Init(); PA_InitVBL(); PA_Init3D(); glEnable(GL_ANTIALIAS); glEnable(GL_TEXTURE_2D); // Habilita el uso de texturas glGenTextures(1, &textureID); //Genera una textura sobre textureID glBindTexture(0, textureID); // Vincula la textura glTexImage2D( // Asigna sobre la textura 0 la textura Crate 0, 0, GL_RGB, TEXTURE_SIZE_128 , TEXTURE_SIZE_128, 0, TEXGEN_TEXCOORD, (u8*)Crate); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(35, 256.0 / 192.0, 0.1, 100); gluLookAt( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); while (1) { glPushMatrix(); Fernando Garcia Bernal 60 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib DrawGLScene(); glPopMatrix(1); glFlush(0); PA_WaitForVBL(); } return 0; } int DrawGLScene() { // Inicializa la matriz de texturas glMatrixMode(GL_TEXTURE); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // Establece las propiedades de los materiales 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)); glTranslatef(0.0f,0.0f,-4.0f); glRotatef(rtri,0.6f,1.0f,0.8f); glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE); glBindTexture(0, textureID); // Carga la textura 0 glBegin(GL_QUADS); // Cara frontal glNormal3f( 0.0f, 0.0f, 1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, // Cara trasera glNormal3f( 0.0f, 0.0f,-1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, // Cara superior glNormal3f( 0.0f, 1.0f, 0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, // Cara inferior glNormal3f( 0.0f,-1.0f, 0.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, Fernando Garcia Bernal -1.0f, -1.0f, 1.0f, 1.0f, 1.0f); 1.0f); 1.0f); 1.0f); -1.0f, 1.0f, 1.0f, -1.0f, -1.0f); -1.0f); -1.0f); -1.0f); 1.0f, -1.0f); 1.0f, 1.0f); 1.0f, 1.0f); 1.0f, -1.0f); -1.0f, -1.0f); -1.0f, -1.0f); -1.0f, 1.0f); 61 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, // Cara derecha glNormal3f( 1.0f, 0.0f, 0.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, // Cara izquierda glNormal3f(-1.0f, 0.0f, 0.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, glEnd(); Librería PAlib -1.0f, 1.0f); -1.0f, -1.0f); 1.0f, -1.0f); 1.0f, 1.0f); -1.0f, 1.0f); -1.0f, -1.0f); -1.0f, 1.0f); 1.0f, 1.0f); 1.0f, -1.0f); rtri+=0.9f; rquad-=0.75f; return TRUE; } Es necesario incluir la textura en el programa de alguna manera. En nuestro caso partimos de una imagen textura.bmp y es necesario convertirla a un formato inteligible por NDS. Para esto usamos una herramienta homebrew llamada TexConv. Existen otras muchas como PAGfxconverter del creador de PAlib, pero con esta es suficiente. En el directorio de códigos, en la ruta /Software/TexConv se encuentra la aplicación. Para poder convertir la textura es necesario copiarla en dicho directorio y modificar el fichero build.bat. La línea de ejecución tiene la siguiente estructura: TexConv {textura a convertir} {textura destino}. Para este caso la textura origen se llama textura.bmp y como destino le damos el nombre Crate.bin. Ejecutando el archivo build.bat comprobamos que sale una ventana ms-dos que si todo es correcto mostrará un mensaje como el siguiente: 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. Fernando Garcia Bernal 62 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib En el código se hace referencia al fichero “Crate.h” que obviando el cambio de extensión se trata de nuestra textura. Después aparece una variable global textureID. Esta será la referencia que tengamos sobre la textura, sobre ella se generará la imagen y se usará cuando haga falta cargarla. La siguiente línea de interés es glEnable(GL_TEXTURE_2D). Como ya se ha comentado, glEnable habilita opciones disponibles, y en este caso posibilita poder hacer uso de texturas. Las siguientes tres líneas se encargar de generar la textura. Con glGenTextures se generan nombres de texturas, el primer parámetro indica el número de nombres de texturas a ser generadas y el segundo el lugar donde serán generadas. En este ejemplo generamos una sola textura en la variable textureID. La función glBindTexture permite crear o generar un nombre de textura. El primer parámetro es el objetivo que en openGL indica el tipo de textura sin embargo, en esta implementación se ignora este parámetro siendo todas las texturas como GL_TEXTURE_2D. El siguiente parámetro es el nombre de la textura a vincular. Cuando se llama a esta función las vinculaciones anteriores dejan de existir y las siguientes operaciones referentes a texturas se aplican sobre la textura vinculada. Para especificar la imagen a la textura vinculada se usa glTexImage2D. Los parámetros pasados son: objetivo, nivel (obviados, sólo aparecen por compatibilidad con openGL), tipo, ancho, alto, borde (también obviado), formato, textura. Aquí se asocia la imagen con la variable global. La siguiente línea de código relacionada con la aparición de texturas se encuentra dentro de la función DrawGLScene. Al igual que se hace con la matriz de modelos de la escena, se carga la matriz de texturas GL_TEXTURE y se inicializa. El siguiente paso es configurar los materiales, en este caso se aplicará a las propiedades de la textura. Estas propiedades se corresponden con la iluminación ambiental, difusa, especular y de emisión. Como ahora no estamos trabajando con iluminación y nos interesa que se vea lo más claro posible, se le dan los máximos valores para que la luz sea blanca. Si por ejemplo se modificarán estos valores se podrían apreciar distintos tonos de colores (¡advertencia! no funciona correctamente en todos los emuladores pero, por supuesto, sí se puede apreciar en el hardware original). Ahora antes de comenzar a pintar las figuras hay que volver a vincular la textura para que esta se aplique sobre los vértices. Se añaden dos funciones nuevas dentro de glBegin y glEnd. La primera es glNormal3f que se le indica el vector normal con tres float. Esto se usa para que la librería gráfica sepa cómo aplicar la iluminación en función de la posición del foco. La otra función es glTexCoord2f y expresa la correspondencia de la textura con los vértices, se Fernando Garcia Bernal 63 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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. En el ejemplo que se va a mostrar en este apartado se muestra un cubo girado a cuyo alrededor existe una luz que desplaza en torno a él. El foco recorre el perímetro de un cuadrado imaginario situado en los bordes infinitos de la escena, de esta manera se observa como el cubo se iluminar parcialmente por cada cara a medida que se desplaza la luz. Estas son las variables y constantes que intervienen en el ejemplo: // Constantes que definen el estado del movimiento de la luz #define TRAMO_SUPERIOR 0 #define TRAMO_DERECHO 1 #define TRAMO_INFERIOR 2 #define TRAMO_IZQUIERDO 3 Fernando Garcia Bernal 64 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib // Variables que establecen las propiedades de la luz para este ejemplo float xCam = -1.0f, yCam = -1.0f; int tramo = TRAMO_SUPERIOR; float inc = 0.2f; 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. En el bucle principal se encarga de mostrar por pantalla la posición actual de la luz, un mensaje para indicar que si se pulsa el botón Start se desactiva la luz, pinta la escena, y actualiza la posición de la luz en función de su posición actual y del tramo que se encuentre recorriendo: while (1) { PA_ClearTextBg(1); // Muestra por pantalla la posición actual de la luz PA_OutputText(1,1,1,"Pos x %f3: ",xCam); PA_OutputText(1,1,2,"Pos y %f3: ",yCam); glPushMatrix(); DrawGLScene(); glPopMatrix(1); glFlush(0); PA_WaitForVBL(); // Actualiza la posición de la luz switch(tramo){ case TRAMO_SUPERIOR: if(xCam < 1.0f) xCam += inc; else{ tramo = TRAMO_DERECHO; yCam += inc; } break; case TRAMO_DERECHO: if(yCam < 1.0f) yCam += inc; else{ tramo = TRAMO_INFERIOR; xCam -= inc; } break; Fernando Garcia Bernal 65 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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(); // Crea la luz en la posición indicada por xCam e yCam glLight(0, RGB15(31,0,0) , floattov10(xCam), floattov10(yCam), 0); 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(); // Mientras no esté pulsado Start se activa la luz if(!Pad.Held.Start){ glPolyFmt( POLY_ALPHA(31) | POLY_CULL_BACK | POLY_FORMAT_LIGHT0); } else{ glPolyFmt(POLY_ALPHA(31) | POLY_CULL_BACK); } Fernando Garcia Bernal 66 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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; } 3.3.7 Seleccionar objetos No hay duda que una de las características diferenciadoras de esta videoconsola con respecto a otro hardware es el uso de la pantalla táctil mediante el puntero o Stylus. Como se ha visto es posible usar un registro especial para conocer si se ha pulsado sobre la pantalla y en tal caso es posible saber en qué posición. En el ámbito de 2D es fácil conocer si se ha pulsado sobre un elemento u otro si se conocen las posiciones y dimensiones de los elementos simplemente viendo si la posición del puntero está contenida sobre el área de los elementos. Fernando Garcia Bernal 67 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib 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> Fernando Garcia Bernal // Librería usada para detectar posiciones 68 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib // Definición de tipos typedef enum { NOTHING, RED, GREEN, BLUE } Clickable; // Variables globales Clickable clicked; int closeW; int polyCount; // Contiene qué ha sido pulsado // Distancia más cercana a la cámara // Guarda el número de polígonos dibujados uint32 rotateX = 0; uint32 rotateY = 0; // Guarda la posición pulsada int touchX, touchY; // usado por gluPickMatrix() int viewport[]={0,0,255,191}; // Funciones int DrawGLScene(); // Ejecutada antes de dibujar un objeto durante el recorte void startCheck(); // Ejecutado después de dibujar un objeto durante el recorte void endCheck(Clickable obj); void drawCube(float x, float y, float z, float r, float g, float b); int main() { PA_Init(); PA_InitVBL(); PA_Init3D(); PA_InitText(1,3); // Habilita dibujar el borde, para mostrar el objeto seleccionado glEnable(GL_OUTLINE); // Establece el color del borde glSetOutlineColor(0,RGB15(31,31,31)); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(35, 256.0 / 192.0, 0.1, 100); gluLookAt( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); while(1) Fernando Garcia Bernal 69 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib { 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(); // Actualiza la rotación si se pulsa algún cursor if(Pad.Held.Up) rotateX += 3; if(Pad.Held.Down) rotateX -= 3; if(Pad.Held.Left) rotateY += 3; if(Pad.Held.Right) rotateY -= 3; // Obtiene la posición pulsada touchX = Stylus.X; touchY = Stylus.Y; } 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 } void endCheck(Clickable obj) { while(GFX_BUSY); // Esepera a que los polígonos sean dibujados¡ while(PosTestBusy()); // Espera a que la posición de prueba termine if(GFX_POLYGON_RAM_USAGE>polyCount) // Si se ha dibujado algún // polígono más.. { if(PosTestWresult()<=closeW) { // Si el objeto actual es el más cercano bajo el cursor.. closeW=PosTestWresult(); clicked=obj; } } Fernando Garcia Bernal 70 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib } int DrawGLScene(){ glViewPort(0,0,255,191); // Establece la vista a la pantalla completa glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(30, 256.0 / 192.0, 0.1, 20); glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Salva el estado de modelview { glTranslate3f32(0,0,floattof32(-6)); glRotateXi(rotateX); glRotateYi(rotateY); glPushMatrix(); // Salva el estao de modelview antes de la // primera pasada { // Dibuja la escena para mostrarla 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 // Dibuja la escena recortada { clicked = NOTHING; // Reset de clicked closeW = 0x7FFFFFFF; // Reset de la distancia Fernando Garcia Bernal 71 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Librería PAlib // Deja la vista fuera de la pantalla, así se oculta // todos los render que se hagan durante esta fase glViewPort(0,192,0,192); 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; } void drawCube(float x, float y, float z, float r, float g, float b){ glTranslate3f32(floattof32(x),floattof32(y),floattof32(z)); glColor3f(r,g,b); glBegin(GL_QUADS); // Cara frontal glVertex3f(-1.0f, glVertex3f( 1.0f, glVertex3f( 1.0f, glVertex3f(-1.0f, // Cara trasera glVertex3f(-1.0f, glVertex3f(-1.0f, glVertex3f( 1.0f, glVertex3f( 1.0f, // Cara superior glVertex3f(-1.0f, glVertex3f(-1.0f, glVertex3f( 1.0f, glVertex3f( 1.0f, // Cara inferior glVertex3f(-1.0f, glVertex3f( 1.0f, glVertex3f( 1.0f, glVertex3f(-1.0f, // Cara derecha Fernando Garcia Bernal -1.0f, -1.0f, 1.0f, 1.0f, 1.0f); 1.0f); 1.0f); 1.0f); -1.0f, 1.0f, 1.0f, -1.0f, -1.0f); -1.0f); -1.0f); -1.0f); 1.0f, -1.0f); 1.0f, 1.0f); 1.0f, 1.0f); 1.0f, -1.0f); -1.0f, -1.0f); -1.0f, -1.0f); -1.0f, 1.0f); -1.0f, 1.0f); 72 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS glVertex3f( 1.0f, glVertex3f( 1.0f, glVertex3f( 1.0f, glVertex3f( 1.0f, // Cara izquierda glVertex3f(-1.0f, glVertex3f(-1.0f, glVertex3f(-1.0f, glVertex3f(-1.0f, glEnd(); Librería PAlib -1.0f, -1.0f); 1.0f, -1.0f); 1.0f, 1.0f); -1.0f, 1.0f); -1.0f, -1.0f); -1.0f, 1.0f); 1.0f, 1.0f); 1.0f, -1.0f); } 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. Fernando Garcia Bernal 73 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fernando Garcia Bernal Librería PAlib 74 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Análisis del Juego Capítulo 4. Fase de Análisis del Juego Aquí comienza la segunda parte de la memoria de este proyecto. La primera parte, ya explicada, abarca todos los aspectos necesarios, que se podrían considerar como introducción aportando los conocimientos y herramientas necesarias para llevar a cabo un desarrollo completo. Que para este caso se considera como producto final, la elaboración de un juego. Este capítulo se centra en la fase de análisis, fundamental en cualquier elaboración que contenga una arquitectura. Se parte de una necesidad y a partir de esta se toman las decisiones necesarias para llevar a la práctica un proyecto real. Como se trata de un desarrollo en el que el autor de la idea es la misma persona que va a llevarlo hasta últimos términos, se va a comenzar desde la base primigenia, es decir, desde la toma de decisiones de qué idea se quiere convertir en juego. 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 Fernando Garcia Bernal 75 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Análisis del Juego 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. Fernando Garcia Bernal 76 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Análisis del Juego 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? 4.3 Modelado conceptual Ya está definida la idea del juego. Esta no abarca todos los detalles, por tanto en esta fase también se asumen decisiones. 4.3.1 Fases del juego Esta aventura se va a componer de dos fases. La primera consiste en una habitación. Será donde aparezca por primera vez el personaje. Se trata de un escenario cerrado y delimitado por las paredes que lo componen. En ella se pueden encontrar una serie de objetos con los que Fernando Garcia Bernal 77 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Análisis del Juego 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. Fernando Garcia Bernal 78 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Análisis del Juego 4.4 Diagrama de casos de uso Conociendo como se va a desarrollar la partida, ya se pueden definir los diagramas de casos de uso. Aunque los posibles estados no presentan muchas posibilidades diferentes, es interesante dividirlos según conceptos: diagrama de eventos del usuario y diagrama del actor. 4.4.1 Diagrama de casos de uso del actor Este diagrama muestra las acciones generales que puede realizar el personaje protagonista. Se resumen las posibles acciones: • Ir a: El personaje se desplaza hacia una zona del escenario o bien hacia un objeto con el que pueda interactuar. • Recoger: Si se encuentra activada esta acción y se selecciona un objeto, este es almacenado en el inventario del protagonista. Existe la posibilidad de que no se pueda recoger pero dependerá del guión de la aventura. • Usar: Podrá utilizar un objeto para avanzar en la aventura. Existen dos posibilidades: usar un objeto que se encuentre en el escenario, por ejemplo una mesa para desplazarla; o bien combinar un objeto de su inventario junto con otro que se encuentre en el escenario, por ejemplo usar una llave que haya encontrado junto con una puerta. 4.4.2 Diagrama de casos de uso del usuario El diagrama de los eventos que realiza el usuario durante la partida es de la siguiente manera: Por tanto, quedan los siguientes eventos: Fernando Garcia Bernal 79 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS • Fase de Análisis del Juego 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. Fernando Garcia Bernal 80 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño Capítulo 5. Fase de Diseño En el capítulo anterior ya se dejó finalizada la fase de análisis. Esta se abstrajo de relacionarse con ningún componente de más bajo nivel que organizar las ideas del producto final. Esta fase de diseño debe tomar los análisis de la etapa anterior y realizar un diseño exhaustivo de los componentes que componen la idea, para que después en la etapa posterior de implementación esté todo tan claro como para no tener que replantear ningún aspecto. 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. 5.1 Diseño de Personaje y Objetos La parte visible de la aplicación es con la que tiene que lidiar el usuario, es importante hacerla atractiva ya que se trata de un producto que debe satisfacer al cliente. Los dos aspectos más importantes con los que un usuario trata directamente en este juego en el aspecto gráfico son: la interfaz gráfica de usuario y los elementos que componen las escenas. En este punto se muestran los diseños de estos últimos. 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 Fernando Garcia Bernal 81 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño 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. 11 - Dibujo frente y perfil del personaje Protagonista. 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. 5.2 Diseño de la Interfaz del Usuario La interfaz de usuario es la encargada de interactuar con el usuario y la lógica de la aplicación. Debe mostrar de una manera clara e intuitiva las posibles acciones que puede llevar a cabo el usuario de la aplicación. Para saber cuáles son las posibles acciones a realizar se puede observar el diagrama de casos de uso del usuario. La figura 12 representa a la interfaz de usuario es la siguiente: Fernando Garcia Bernal 82 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño 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. • : Selecciona la acción ‘Ir a’ • : Selecciona la acción ‘Usar’ • : Selecciona la acción ‘Recoger’ 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. Fernando Garcia Bernal 83 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño Para indicar al usuario la acción en la que se encuentra, además de mostrar en el rótulo inferior el texto que lo explique, es necesario hacer uso de un selector que resalte la acción en concreto. Para remarcar la acción que se encuentra seleccionada se usa la imagen 13 y para saber que objeto se tiene seleccionado del inventario, la imagen 14. 13 - Selector acción. 14 - Selector inventario. 5.3 Estudio de las clases El estudio de las clases será el transmitido al equipo de desarrollo para que implementen las clases que componen el código del juego. Este diagrama describe las clases y objetos de la aplicación y las relacionen que lo componen. Cada clase está compuesta por atributos y métodos encapsulados con diferente visibilidad. 5.3.1 Diagrama de clases Para la representación del conjunto de clases, se emplea el estándar UML [23]. Donde una clase se muestra como un rectángulo dividido en tres partes: nombre de la clase, atributos y métodos. 15 - Clase UML. A continuación se muestra el diagrama de clases. Fernando Garcia Bernal 84 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño 5.3.2 Definición de las clases Para entender mejor el esquema de clases, se muestran agrupadas por criterios de funcionalidad. En primer lugar se observa el diagrama de clases que corresponde con la representación de un objeto 3D en el escenario. Fernando Garcia Bernal 85 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño 16 - Estructura UML de la clase Entidad. • 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). El siguiente diagrama muestra la clase ‘Escenario’, la más importante de la aplicación. Fernando Garcia Bernal 86 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño 17 - Estructura UML de la clase Escenario. Fernando Garcia Bernal 87 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño La clase Escenario es el centro neurálgico de la aplicación. En esta se relaciona tanto la escena, como los objetos que la componen como el personaje protagonista. La escena, que se corresponde con el lugar donde el personaje se encuentra, se almacena en la variable Suelo. Los objetos que pueden interactuar con el protagonista se recogen en los vectores elementosSeleccionables y elementosNoSeleccionables. Por último el personaje principal se corresponde con la variable Protagonista. 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. Se observa como las clases EscenarioHabitación y EscenarioExterior son especializaciones de Escenario e implementan los métodos virtuales usar, recoger y eventoFinalizarAnimacion que cada escenario en particular debe saber implementar para responder a los eventos correspondientes según se desarrolle la partida. Además estos escenarios contienen un puntero a cado uno de los elementos que componen la escena de una clase que especializa a Entidad. 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. Fernando Garcia Bernal 88 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fase de Diseño 18 - Relaciones para las clases personalizadas de Escenario. Fernando Garcia Bernal 89 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fernando Garcia Bernal Fase de Diseño 90 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. 6.1 Recursos gráficos Empezamos por estudiar cómo se obtienen los componentes gráficos de la aplicación, ya que hará falta invocarlos cuando nos centremos en el desarrollo del código del juego. Fernando Garcia Bernal 91 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementació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. 6.1.2 Creación objetos 3D El objetivo consiste en obtener la representación de los modelos en 3D que nos interesen, en unos ficheros que puedan ser importados y representados en nuestra aplicación. El problema consiste en que los programas que se utilizan para modelar no aportan ninguna herramienta para exportar a un formato que se pueda usar directamente en la elaboración de aplicaciones para Nintendo DS. Por tanto, se debe usar alguno de los formatos para los que sí exporta, y realizar nosotros mismos la conversión que necesitamos. Realizaremos un paso a paso para poder conseguir que nuestro personaje protagonista pueda ser cargado en la aplicación que queremos implementar. Fernando Garcia Bernal 92 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementació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. 19 - Antes de crear el modelo 3D se sitúan las imágemes de referencia. Fernando Garcia Bernal 93 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 20 - Generación de modelo basándose en las referencias. 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. Fernando Garcia Bernal 94 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. Fernando Garcia Bernal 95 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. 25 - Frame 10. 26 - Frame 14. 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. 6.1.2.1 Formato DirectX Un malla poligonal es una lista estructurada de puntos (llamados vértices) conectados que describen una superficie. En nuestro caso, la malla define la superficie de nuestro modelo. Para aplicar una textura al modelo, asociamos las coordenadas de la imagen con cada vértice Fernando Garcia Bernal 96 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación para conocer que parte de la imagen pintar sobre la figura. Cada cara de la malla puede ser asociada a un material diferente. El formato propuesto por DirectX describe la geometría, jerarquía estructural y animaciones del modelo en cuestión. Este formato se puede elaborar en texto plano o en fichero binario de datos. Aquí usaremos el modo de texto. Se organiza en bloques que describen elementos del modelo. En las páginas de MSDN (Microsoft Developer Network [28]) se puede acceder a las descripciones de todos los elementos detallados. Las llaves son los elementos que delimitan cada bloque. Existen diferentes tipos de bloques que describen distintas partes, y algunos de estos deben estar contenidos dentro de otros. En la tabla 1 se aprecia esta jerarquía: Bloque Frame Contiene FrameTransformMatrix Frame Mesh Mesh MeshNormals MeshTextureCoords MeshMaterialList SkinMeshHeader SkinWeights MeshMaterialList Material Material TextureFileName AnimationSet Animation Animation AnimationKey Tabla 1 - - Estructura fichero X Vamos a estudiar la estructura de un fichero X basándonos en un ejemplo sencillo. El modelo se puede ver en la figura 27. Fernando Garcia Bernal 97 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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 xof 0303txt 0032 template FVFData { <b6e70a0e-8ef9-4e83-94ad-ecc8b0c04897> DWORD dwFVF; DWORD nDWords; array DWORD data[nDWords]; } template EffectInstance { <e331f7e4-0559-4cc2-8e99-1cec1657928f> STRING EffectFilename; [...] } Fernando Garcia Bernal 98 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. Fernando Garcia Bernal 99 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación Adicionalmente se puede añadir la subplantilla TextureFilename para indicar una textura al material, como se hace en este caso con los dos materiales que se muestran a continuación. 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 { Fernando Garcia Bernal 100 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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; Fernando Garcia Bernal 101 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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;; } } } Fernando Garcia Bernal 102 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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 Fernando Garcia Bernal 103 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación bloque es el número de coordenadas de la malla, y después aparece el listado de correspondencia entre cada coordenada del modelo, con una coordenada de la textura. 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; Fernando Garcia Bernal 104 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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;;; } Este último bloque se encarga de definir como se realiza la animación de un modelo. La primera línea indica un nombre a la animación, en el ejemplo ‘Anim-cabeza’ que como se entiende es la animación de la figura que hemos denominado ‘cabeza’, lo cual se comprueba porque en la siguiente línea aparece la figura que está asociada a este bloque. A su vez se compone de un bloque anidado llamado ‘AnimationKey’ donde se encuentra la información importante. Esta estructura que pasamos a explicar ahora se usa tanto para las transformaciones de rotación, escalado y translación. Es posible diferenciar cuándo se trata de cada una, por el primer dígito que aparece. Si se trata de un 0, entonces describe una animación de rotación, si es 1 entonces escalado y si fuera 2 se trataría de translación. Para cualquiera de los tres casos, el siguiente valor que aparece es la cantidad de frames que compone la animación, en el caso del ejemplo son 41 frames (que van de 0 a 40). A partir de aquí el contenido del subloque ‘AnimationKey’ es la cantidad de la transformación. Se compone por un dígito que indica el frame, un valor entero que indica el número de valores decimales que describen la transformación y por último dichos valores. En el caso de la rotación, se elabora con cuaterniones, que se explicarán en la implementación del juego; ahora basta con saber que un cuaternión describe un giro en tres dimensiones pero se especifica con cuatro componentes. Para el caso del escalado y la translación se usan tres componentes en lugar de cuatro. 6.1.2.2 Conversión de formato Ya conocemos como se obtiene un objeto modelado con una herramienta de diseño. Ahora la parte siguiente es transformarlo a una estructura de datos que pueda interpretar nuestra aplicación para poder representarlo con primitivas openGL. Para ello se ha construido una Fernando Garcia Bernal 105 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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 Fernando Garcia Bernal 106 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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; Fernando Garcia Bernal 107 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación int nMateriales; material *mat; }; • Implementación Esta aplicación de escritorio presenta la siguiente interfaz mostrada en la figura 28. 28 - Aspecto de la interfaz conversor ficheros X. 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: void convertirArchivos(char *rutaX, char *rutaDestino, char *nombre, char *salida) 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. • Lectura del fichero X Esta primera parte consiste en ir leyendo el fichero explicado previamente, identificando cada bloque y almacenarlo en la estructura de datos que hemos declarado. En la siguiente fase se volcará la información interpretada en un fichero binario que cargaremos desde la aplicación. Se distinguen tres diferentes bloques a un mismo nivel de anidamiento: ‘Material’, ‘Animation’ y ‘Frame’. Fernando Garcia Bernal 108 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. Si al leer el fichero de texto, se encuentra la palabra ‘Animation’, entonces se carga el bloque correspondiente. Primero se guarda el nombre de la figura de la que se va a describir su animación, después se localiza el objeto figura y sobre él se actualiza el número de frames con la información que se recupera del fichero X. Por último se guarda en una estructura animacion, la información relativa a la rotación, escalado y translación de la figura correspondiente. 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. • Creación fichero binario El fichero X de texto deja aquí de tener utilidad. Ya se ha leído su contenido y almacenado en las estructuras de datos pertinentes. El siguiente paso es persistir esta información en un fichero de datos que mantenga una estructura definida que podamos usar en la aplicación 3D para pintar los modelos usando primitivas openGL. A continuación se muestra el código capaz de generarlo: /************************************ Fernando Garcia Bernal 109 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * Crear fichero .bin ************************************/ char nomfich[MAXCAD]; sprintf(nomfich,"%s\\%s.bin",rutaDestino,mod.nombre); // Creo el fichero binario ofstream fos(nomfich,ios::out|ios::binary); // Guardo la estructura modelo fos.write(reinterpret_cast<char *>(&mod),sizeof(modelo)); // A continuación se concatenan las estructuras material que // le correspondan for(int i=0; i<mod.nMateriales; i++) { material maux=mat[i]; fos.write(reinterpret_cast<char *>(&maux),sizeof(material)); } for(int i=0; i<mod.nFiguras; i++) { // Después por cada figura… figura f=fig[i]; // Se guarda la información estática con la que cuenta // la estructura figura fos.write(reinterpret_cast<char *>(&f), sizeof(figura)); // Se guarda la lista con el índice de cada material y las // veces que se usa fos.write(reinterpret_cast<char *>(f.numeroMat), sizeof(int)*f.nMateriales); fos.write(reinterpret_cast<char *>(f.vecesMat), sizeof(int)*f.nMateriales); // Aquí se guarda la lista de vértices. El número // de vértices es igual al número de caras multiplicado por // tres (número de vértices por cada cara) y multiplicado por // cinco (porque son cinco float para representar las // componentes (x,y,z) y las componentes (u,v) de la textura. fos.write(reinterpret_cast<char *>(f.v), sizeof(float)*f.nCaras*3*5); // Finalmente se almacena la animación fos.write(reinterpret_cast<char *>(f.a), sizeof(animacion)*f.nFrames); } 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. Fernando Garcia Bernal 110 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 6.2 Arquitectura de la aplicación Ahora la implementación se enfoca sobre el código que permite crear un ejecutable para la videoconsola Nintendo DS. Ya se han creado los recursos necesarios que deben ser importados por este código. En este paso se hace uso tanto de algoritmos ad hoc como de las funcionalidades explicadas de las librerías PAlib y libnds. Este apartado se descompone en las diferentes partes que forman el código de la aplicación. La sección librerías trata sobre las herramientas que se han implementado como utilidades de manejo de vectores, cuaterniones,… Después continúa con cada una de las clases que se definieron en el capítulo de diseño, se irán tratando según van apoyándose unas clases a otras mediante la herencia siendo en mi opinión la manera más didáctica de comprenderlo. El último subapartado trata de cómo se ha implementado el fichero principal main, valga la redundancia. 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: Fernando Garcia Bernal 111 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** * 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 /** Constructor vacío */ vector2(); /** Constructor con inicialización de valores float */ vector2(float _x, float _y); ~vector2(); public: // Operadores /** Asignación */ vector2 &operator = (const vector2 &v); /** Suma un vector2 a este */ vector2 &operator += (const vector2 &v); /** Resta un vector2 a este */ vector2 &operator -= (const vector2 &v); /** Multiplica el vector2 por un float */ vector2 &operator *= (float f); /** Divide el vector2 entre un float */ vector2 &operator /= (float f); /** ¿Son iguales estos dos vector2? */ friend bool operator == (const vector2 &a, const vector2 &b); /** ¿Son diferentes estos dos vector2? */ friend bool operator != (const vector2 &a, const vector2 &b); /** Resta dos vector2 */ friend vector2 operator - (const vector2 &a, const vector2 &b); /** Suma dos vector2 */ friend vector2 operator + (const vector2 &a, const vector2 &b); /** Multiplica un vector2 por un float */ friend vector2 operator * (const vector2 &v, float f); /** Multiplica un vector2 por un float */ friend vector2 operator * (float f, const vector2 &v); /** Divide un vector2 entre un float */ friend vector2 operator / (const vector2 &v, float f); Fernando Garcia Bernal 112 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** Niega un vector2 */ friend vector2 operator - (const vector2 &a); public: // Métodos /** Obtiene el tamaño de un vector2 */ float length() const; /** Normaliza un vector2 */ vector2 &normalize(); /** Gira 90 grafos un vector2 */ vector2 &girar90(); /** Devuelve la distancia entre una coordenada y esta */ float distancia(const vector2 &v); }; Se observan constructores vacíos, con inicialización de parámetros; operadores de asignación, adición, resta, multiplicación, división; y métodos para obtener la longitud, normalizar, girar 90 grados el vector y para obtener la distancia de una coordenada a otra. • 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 /** Constructor vacío */ vector3() {}; /** Constructor con inicialización de valores float */ vector3(float inX, float inY, float inZ); public: // Operadores Fernando Garcia Bernal 113 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS /** Asignación */ vector3 &operator = Implementación (const vector3 &v); /** Asignación desde vector2 */ vector3 &operator = (const vector2 &v); /** Suma un vector3 a este */ vector3 &operator += (const vector3 &v); /** Resta un vector3 a este */ vector3 &operator -= (const vector3 &v); /** Multiplica el vector3 por un float */ vector3 &operator *= (float f); /** Divide el vector3 entre un float */ vector3 &operator /= (float f); /** ¿Son iguales estos dos vector3? */ friend bool operator == (const vector3 &a, const vector3 &b); /** ¿Son diferentes estos dos vector3? */ friend bool operator != (const vector3 &a, const vector3 &b); /** Niega un vector3 */ friend vector3 operator - (const vector3 &a); /** Suma dos vector3 */ friend vector3 operator + (const vector3 &a, const vector3 &b); /** Resta un vector3 a otro */ friend vector3 operator - (const vector3 &a, const vector3 &b); /** Multiplica un vector3 por un float */ friend vector3 operator * (const vector3 &v, float f); /** Multiplica un vector3 por un float */ friend vector3 operator * (float f, const vector3 &v); /** Divide un vector3 entre un float */ friend vector3 operator / (const vector3 &v, float f); public: // Métodos /** Obtiene el tamaño de un vector3 */ float length() const; /** Normaliza un vector3 */ vector3 &normalize(); }; 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 Fernando Garcia Bernal 114 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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 Fernando Garcia Bernal 115 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * @param grados representa el angulo por el que se quiere formar la * rotacion */ void CreateFromAxisAngle(float x, float y, float z, float grados); /** * 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 float float float m_w; m_z; m_y; m_x; }; Lo más utilizado de esta librería es la llamada al constructor de cuaterniones a partir de sus cuatro componentes float y el uso de los operadores +, - y *. 6.2.1.3 Librería lib Dentro de esta librería se han incluido constantes, las estructuras de datos utilizadas en la aplicación y varias funciones. Estos elementos no se han podido asociar a una entidad lógica concreta y por tanto se colocan en esta librería genérica. • Constantes Estas constantes se usan para indicar longitudes máximas: Fernando Garcia Bernal 116 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** usado para el algoritmo A estrella */ #define MAX 1000 /** define la máxima longitud de cadena */ #define MAXCAD 500 • Tipos de datos Algunas de estas estructuras ya se han comentado y otras se usarán más adelante: /** almacena los vértices de una cara */ typedef float vert[3][5]; /** define un material */ typedef struct{ char nombre[MAXCAD]; char fichero[MAXCAD]; int ancho, alto; int veces; // // // // // // nombre del material nombre del fichero donde se almancena el material componentes ancho y alto número de caras en las que se usa el material } material; /** define una animación */ typedef struct{ float rot[4], // rotación {x,y,z,w} en forma de cuaternion, por // eso son 4 componentes sca[3], // escalado {x,y,z} pos[3]; // posición {x,y,z} } animacion; /** define el estado en el que se puede encontrar un objeto */ typedef struct{ float pos[3], // posición {x,y,z} rot[3], // rotación {x,y,z} esc[3]; // escalado {x,y,z} } estado; /** Estructura de un nodo usada para el algoritmo A* */ typedef struct TNodo{ int loc; // número del nodo en carasDesplazables int indice; // índice de la posición en carasDesplazables float costFromStart; // coste desde el inicio hasta este nodo float costToGoal; // coste desde este nodo al destino float totalCost; // coste total TNodo *parent; // nodo padre } nodo; • Funciones Fernando Garcia Bernal 117 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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(); La implementación de estas funciones es la siguiente: void LoadTex(u8* texture, int id, int sizeX, int sizeY) { 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) Fernando Garcia Bernal 118 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementació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); // Carga el selector de botones PA_CreateSprite(1, 0, (void*)selector_Sprite, OBJ_SIZE_32X32, 1, 0, 256, 0); // Carga el selector de elementos del inventario. Se realiza // dentro de un bucle ya que es un srpite reducido que debe // repetirse a lo ancho for(int i=1; i<=3; i++) { PA_CreateSprite(1, i, (void*)selectorItem_Sprite, OBJ_SIZE_32X16, 1, 0, 256, 0); PA_SetSpritePrio(1, i, 3); } } void inicializaEscena3D(){ glEnable(GL_ANTIALIAS); // Habilita dibujar el borde, para mostrar el objeto // seleccionado glEnable(GL_OUTLINE); // Establece el color del borde glSetOutlineColor(0,RGB15(31,31,31)); // Establece la vista a la pantalla completa glViewPort(0,0,255,191); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(35, 256.0 / 192.0, 0.1, 20); glMatrixMode(GL_TEXTURE); glLoadIdentity(); Fernando Garcia Bernal 119 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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); // Desactiva la iluminación del contorno de las Entidades glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(0)); } void reiniciarEscena() { PA_ClearTextBg(1); // desplaza los sprites selector de // botones y selector de inventario // al margen derecho para que no // se muestren for(int i=0; i<3; i++) { PA_SetSpriteX(1,i+1,255); } } 6.2.2 Clase Figura La clase figura está definida exactamente igual que la que se definió para la generación del fichero binario que representa al modelo 3D. Así se consigue que se pueda realizar una asignación desde el puntero que apunta al fichero binario generado, hacia un objeto de la clase Figura simplemente realizando un casting. Esto se puede ver en el apartado que explica la clase Objeto3D que es quién se encarga de cargar el objeto desde la memoria. Esta es la definición: /** * 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: Fernando Garcia Bernal 120 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** nombre de la figura */ char nombre[MAXCAD]; /** número de frames que compone la figura */ int nFrames; /** número de caras que compone la figura */ int nCaras; /** número de puntos que compone la figura */ int nPuntos; /** * número de materiales ó texturas que compone la figura */ int nMateriales; /** vector con los índices de los materiales */ int *numeroMat; /** vector con los números de veces que se repite cada material */ int *vecesMat; /** conjunto de vértices que dibujan a la figura */ float *v; /** conjunto de frames que componen la animación */ animacion *a; Figura(); ~Figura(); }; 6.2.3 Clase Objeto3D Esta clase también tiene correspondencia con la otra clase que se encontraba en el programa que generaba el fichero binario a partir del modelo 3D. De nuevo el motivo es evidente, la carga a partir del fichero de datos se simplifica mucho al compartir la misma clase. La única diferencia entre las dos clases es su nombre, aquí se llama Objeto3D y en la otra aplicación se trata de modelo. Su definición en Objeto3D.h es la siguiente: /** * 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: /** nombre del objeto 3D */ char nombre[MAXCAD]; /** número de figuras que componen el objeto */ int nFiguras; Fernando Garcia Bernal 121 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** vector de figuras que componen el objeto */ Figura *f; /** número de materiales que componen el objeto */ int nMateriales; /** vector de materiales que componen el objeto */ material *mat; /** Constructor vacío */ Objeto3D(){ memset(nombre, '\0', MAXCAD); nFiguras=0; nMateriales=0; }; /** * 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. void Objeto3D::cargarObjeto3D(const void *_nombre){ // Se lee el objeto Objeto3D del fichero Objeto3D *ptr = (Objeto3D*)_nombre; // Después de cargar el objeto es necesario // cargar manualmente los datos que están apuntados // desde un puntero para acceder a las direcciones // de memoria sprintf(nombre,"%s",ptr->nombre); nMateriales = ptr->nMateriales; nFiguras = ptr->nFiguras; // se avanza el puntero el tamaño de Objeto3D ptr=(Objeto3D*)(((int)ptr)+sizeof(Objeto3D)); Fernando Garcia Bernal 122 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // carga de la lista de materiales mat=(material *)ptr; ptr=(Objeto3D*)(((int)ptr)+sizeof(material)*nMateriales); // crea array de figuras f=new Figura[nFiguras]; // se carga cada figura for(int posF=0; posF<nFiguras; posF++) { // lee los elementos que no son punteros de Figura f[posF]=*(Figura *)ptr; ptr=(Objeto3D*)(((int)ptr)+sizeof(Figura)); // lee los materiales de la figura f[posF].numeroMat=(int *)ptr; ptr = (Objeto3D*) (((int)ptr) + sizeof(int) * f[posF].nMateriales); f[posF].vecesMat=(int *)ptr; ptr = (Objeto3D*) (((int)ptr) + sizeof(int) * f[posF].nMateriales); // lee los vértices de la figura. El tamaño para avanzar el // puntero en la figura será el tamaño de float por el // número de caras multiplicadas por tres (tres vértices // por cara) y multiplicado por cinco float (coordenadas // (x,y,z) y de la textura (u,v)) f[posF].v=(float *)ptr; ptr = (Objeto3D*) (((int)ptr) + sizeof(float) * f[posF].nCaras*3*5); // carga las animaciones, tantas como nFrames f[posF].a=(animacion *)ptr; ptr = (Objeto3D*) (((int)ptr) + sizeof(animacion) * f[posF].nFrames); } } 6.2.4 Clase Entidad La clase Entidad representa un objeto que se puede encontrar en el escenario. Extiende de la clase Objeto3D y a diferencia de esta, la clase Entidad ya confiere al objeto de propiedades propias de un elemento del juego. Esta tiene un estado que puede ser modificado durante la partida, definido por una posición, traslación y rotación. También cabe la posibilidad de que pueda iluminarse su contorno, capacidad que se utiliza cuando el usuario selecciona el objeto por ejemplo. En un instante del juego puede comenzar a realizar una animación que se hubiera definido en el diseño del objeto, por tanto dispone de una variable que representa el frame actual. Además, según las necesidades de la aventura, puede establecerse que una entidad pueda ser recogida del escenario, con lo cual desaparecería de la escena y se almacenaría en el inventario del protagonista; o también un objeto puede tener la Fernando Garcia Bernal 123 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementació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: /** vector con los id en los cuales se vincula las texturas */ int *texId; /** estado Actual */ estado instante; /** frame Actual */ int fr; /** número total de frames que compone la entidad */ int nFrames; /** indica si iluminar el objeto. Usado para testear */ bool iluminar; /** tamaño de la textura, por defecto es 128 */ int tamanoText; /** cara del suelo más cercana a la entidad, dónde se desplazará el protagonista para alcanzar la entidad */ int caraMasCercana; /** indica si el objeto se puede recoger */ bool puedeRecogerse; /** variable de uso opcional para condicionar el uso de una entidad dentro del método virtual usar() */ bool puedeUsarse; /** mensaje que se muestra cuando no se puede recoger la figura. No es obligatorio */ char mensajeNoRecoger[50]; /** mensaje que se muestra cuando no se puede usar la figura. No es obligatorio */ char mensajeNoUsar[50]; 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 Fernando Garcia Bernal 124 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * @param _nombre nombre del archivo binario donde se encuentra * la Entidad * @param _textura nombre de la textura * @param tamanoText tamaño de la textura */ Entidad(const void * _nombre, const void * _textura, int tamanoText); virtual ~Entidad(){}; /** Pinta la Entidad en la escena */ void pintarGL(); /** * 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 */ void inicializar(const void * _textura); /** * Carga las texturas. Es llamado desde inicializar * @param texturas nombre de la textura que se asocia a la * Entidad * @see inicializar */ void cargarTexturas(const void* texturas); /** * Vincula la textura con su id a la hora de pintar la Entidad * en la escena * @param nMat id de la textura de la Entidad que se va a pintar */ void vincularTextura(int nMat); }; 6.2.5 Clase Personaje La clase Personaje es una especialización de la clase Entidad. Realmente no aporta ninguna característica propia a su clase padre, simplemente se ha creado para poder definir que en los métodos que así lo requieran, que acepten una clase de tipo Personaje en lugar de una entidad cualquiera. Este es el caso de un método que se verá en el siguiente apartado y que pertenece a la clase Escenario, requiere que la entidad que se vincule al escenario sea Fernando Garcia Bernal 125 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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 (){}; }; 6.2.6 Clase Escenario Esta puede considerarse la clase más importante de la aplicación. Desde aquí se coordinan todos los elementos que componen el juego. Al escenario se vincula el Personaje, el suelo y las entidades seleccionables y no seleccionables por el usuario. Por tanto, gracias a que esta clase tiene visibilidad sobre todos los elementos, se convierte en el centro y nexo. Posee un método actualizar que hace las funciones de bucle de juego. Esto se refiere al bucle que implementan la mayoría de los juegos donde se realizan las acciones rutinarias básicas: lectura de eventos, actualización de componentes y muestra de información al usuario. A continuación se muestra la definición de esta clase, y posteriormente se pasan a explicar las funciones que incorpora con mayor detalle. /** * 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. Fernando Garcia Bernal 126 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación */ class Escenario { public: /** Entidad que representa el suelo del escenario */ Entidad Suelo; /** * 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; /** indica el elemento seleccionado del inventario */ int seleccionInventario; /** * Mensaje de información a mostrar en la pantalla * superior */ char mensajePantalla[30]; /** Fernando Garcia Bernal 127 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * Tiempo que se mostrará el mensaje por pantalla (Por * defecto -1) */ int tiempoMensajePantalla; /** Entidad que se animará a consecuencia de un evento */ Entidad *entidadEnAnimacion; /** * 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; /** Propiedades del suelo para su correcta visualización */ float inclinacion, distancia, altura; /** * 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 Fernando Garcia Bernal 128 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * @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; /** velocidad de desplazamiento del personaje por frame */ float velocidadDesplazamiento; /** entidad que se desplaza a recoger */ int entidadSeleccionada; /** * 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 Fernando Garcia Bernal 129 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación */ 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 Fernando Garcia Bernal 130 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * @param nombre nombre de la Entidad a comprobar * @return true si se encuentra la entidad en el inventario * false en caso contrario */ bool poseeEnInventario(char *nombre); /** * 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 Fernando Garcia Bernal 131 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * @param tipoElementoPulsado devuelve el tipo de elemento que * se ha pulsado. -1 si no se ha pulsado nada */ void actualizarTextos(int *elementoPulsado, int *tipoElementoPulsado); /** * 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(); } for(unsigned int i=0;i<elementosSeleccionables.size();i++){ elementosSeleccionables[i]->pintarGL(); } glPopMatrix(0); glFlush(0); } Fernando Garcia Bernal 132 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** * 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)); } Fernando Garcia Bernal 133 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación /** * 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 Fernando Garcia Bernal 134 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación * @param goalLoc localización objetivo * @see buscarCaminoAEstrella_Original * @return coste entre dos puntos */ float PathCostEstimate(int startLoc, int goalLoc); /** * 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 */ Fernando Garcia Bernal 135 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación virtual bool recoger(Entidad *obj1) = 0; /** * 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; }; 6.2.6.1 Constructor Escenario Como ya se ha comentado, el escenario será el objeto que coordine los diferentes componentes que forman la partida. Recordemos la signatura del constructor: /** * 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. Fernando Garcia Bernal 136 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS • Implementación 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. 29 - Vista pespectiva de un suelo. 30 - Vista frontal del mismo suelo. 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. Fernando Garcia Bernal 137 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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; cam[0][0] = cam[0][1] = .0f; cam[0][2] = -.5f; //posición cam[1][0] = cam[1][1] = cam[1][2] = .0f; //look at cam[2][0] = .0f; cam[2][1] = 1.0f; cam[2][2] = .0f; //vector alzado inclinacion = _inc; distancia = _dist; altura = _alt; // Se establecen los valores de la variable viewport viewport = (int*)malloc(sizeof(int)*4); viewport[0]=0; viewport[1]=0; viewport[2]=255; viewport[3]=191; // crea la Entidad suelo Suelo = Entidad(_suelo, _textura, _tamanoText); El siguiente paso es identificar las caras por las que se desplaza el personaje protagonista: // Almaceno en carasDesplazables las caras cuyos tres // vértices tienen su componente Y por encima de 0.0. // Esto se debe a que durante el dise&ntilde;o de los suelos hay // que dejar las caras por las que desplaza el personaje por // encima de 0.0. Así se pueden distinguir las caras // desplazables de las caras ocultas. vert *vaux=(vert *)Suelo.f[0].v; for(int c=0; c<Suelo.f[0].nCaras; c++){ if(vaux[c][0][3] > 0.0f && vaux[c][1][3] > 0.0f && vaux[c][2][3] > 0.0f){ carasDesplazables.push_back(c); } } Fernando Garcia Bernal 138 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Para acceder a los vértices se observan llamadas Implementación como: vaux[c][0][3], vaux[c][1][3] ó vaux[c][2][3]. Esto se debe a la distribución de los vértices. La variable vaux es un array multidimensional de esta manera: vaux[caras][vértice de la cara][componente (u,v,x,y,z)]. Recordar que las componentes (u,v) se refieren a la correspondencia entre el modelo 3D y la textura. De esta manera, si se accede a: vaux[c][0][3], vaux[c][1][3] ó vaux[c][2][3], se está devolviendo las tres componentes Y (altura) de los tres vértices que forman la cara c. Como se ve en el código, si los tres vértices de una misma cara, tienen su componente Y por encima de 0, entonces se considera que es una cara por la que se puede desplazar el personaje. Lo siguiente que se calcula son las caras contiguas a cada cara, esto será necesario para cuando sea necesario calcular el camino entre dos puntos. Para realizar estos cálculos se hace uso de la clase vector2 para facilitar las operaciones trigonométricas, y de la función auxiliar perteneceArea que dada una coordenada (x,y) devuelve la cara desplazable a la que pertenece. A continuación su implementación: int Escenario::perteneceArea(vector2 v){ float x = v.x; float y = v.y; int cara = -1; vert *vaux=(vert *)Suelo.f[0].v; for(unsigned int i=0; i<carasDesplazables.size(); i++) { int c = carasDesplazables[i]; // Los 3 vértices que componen la cara vector2 v0(vaux[c][0][2], vaux[c][0][4]); vector2 v1(vaux[c][1][2], vaux[c][1][4]); vector2 v2(vaux[c][2][2], vaux[c][2][4]); int l1, l2, l3; l1=signo((v1.x-v0.x)*(y-v0.y)-(v1.y-v0.y)*(x-v0.x)); l2=signo((v2.x-v1.x)*(y-v1.y)-(v2.y-v1.y)*(x-v1.x)); l3=signo((v0.x-v2.x)*(y-v2.y)-(v0.y-v2.y)*(x-v2.x)); if (l1==l3 && l1==l2 && l2==l3) cara = c; } return cara; } Fernando Garcia Bernal 139 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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: // Calculo las caras contiguas a cada cara cont = (vector<int>*) malloc(sizeof(vector<int>) *carasDesplazables.size()); for(unsigned int i=0; i<carasDesplazables.size(); i++) cont[i] = vector<int>(); int c=-1; vector<int> listaContiguas; for(unsigned int i=0; i<carasDesplazables.size(); i++){ c=carasDesplazables[i]; listaContiguas.clear(); // Compruebo si a cada uno de los 3 lados de la cara c hay // una cara contigua for(int l=0; l<3; l++){ //Vértices que componen el lado AB vector2 v1(vaux[c][l][2], vaux[c][l][4]); vector2 v2(vaux[c][(l+1)%3][2], vaux[c][(l+1)%3][4]); //Calculo el punto medio del lado vector2 pm = v1*0.5+v2*0.5; //Calculo el vector director del segmento AB y lo // normalizo vector2 vd=v1-v2; vd.normalize(); vd/=20.0f; //Giro 90º el vector director vd.girar90(); // Compruebo si hay un área contigua en el // exterior, junto al punto medio, en direccion al // nuevo vector director int areaContigua=perteneceArea(pm-vd); if(areaContigua!=-1 && areaContigua!=int(c)) { Fernando Garcia Bernal 140 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación if(esta(areaContigua, listaContiguas)==-1) listaContiguas.push_back(areaContigua); //Puede ser que la figura contigua no // esté en contacto con esta figura en // su punto medio por tanto compruebo si la // otra figura tiene a esta como contigua y en // caso contrario la a&ntilde;ado if(cont[i].size()>0 && esta(int(c), cont[i])==-1) cont[i].push_back(c); } } cont[i]=vector<int>(listaContiguas); } 6.2.6.2 Ciclo de juego Más adelante se explica cómo debe ser el fichero que debe crear toda la estructura de clases para poder ejecutar esta aplicación, sin embargo es conveniente para poder explicar este punto observar un trozo de fichero que se encarga de hacer que el juego se esté ejecutando continuamente. En cierta parte del fichero main.cpp debe aparecer este código: 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; Fernando Garcia Bernal 141 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // Si está animándose alguna Entidad, entonces no se atiende // a los posibles eventos que se puedan ejecutar if(estado == ESTADO_ANIMACION) { entidadEnAnimacion->fr++; if(entidadEnAnimacion->fr >= finAnimacion-1) { // Si termina la animación.. 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, Fernando Garcia Bernal 142 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementació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(){ if(Pad.Held.Y || Pad.Held.A || Pad.Held.B) { // Si se ha pulsado algo... entidadSeleccionada = -1; tiempoMensajePantalla = -1; // Si la acción en la que se encontraba era USAR // se desmarca el inventario if(accion == ACCION_USAR) { seleccionInventario = -1; for(int i=0; i<3; i++) { PA_SetSpriteXY(1,i+1,255,0); } } } // Se cambia de acción si ha pulsado algún botón if(Pad.Held.Y){ accion = ACCION_IR; } else if(Pad.Held.A){ accion = ACCION_USAR; } else if(Pad.Held.B){ accion = ACCION_RECOGER; } Fernando Garcia Bernal 143 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // Se gira la cámara si ha pulsado // los botones respectivos if(Pad.Held.R && cam[0][0] < 1.5f) cam[0][0]+=.5f; else if(Pad.Held.L && cam[0][0] > -1.5f) cam[0][0]-=.5f; } 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: void Escenario::actualizarStylus(int *elementoPulsado, int *tipoElementoPulsado){ if(Stylus.Held){ entidadSeleccionada = -1; tiempoMensajePantalla = -1; // zonaPulsada comprueba qué se ha pulsado a partir de la // pulsación del Stylus *elementoPulsado = zonaPulsada(Stylus.X, Stylus.Y, tipoElementoPulsado); } } 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: // Tipo elemento seleccionado /** * Se devuelve esta constante cuando el usuario pulse con el puntero * sobre el suelo * @see actualizarStylus */ Fernando Garcia Bernal 144 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación #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. Después de actualizarEventos, se invoca al método actualizarAccion. Este se encarga de realizar las medidas oportunas en función de la acción que se encuentre el juego. Estas son: ACCION_IR, ACCION_RECOGER y ACCION_USAR. Recordamos que estas acciones son elegidas por el usuario pulsando sobre los botones correspondientes y la lógica del juego las ha actualizado en el método antes visto actualizarBotones. Por tanto, nos encontramos en el método actualizarAccion que discrimina según la acción actual para actuar en consecuencia. A continuación el código de la implementación del método: void Escenario::actualizarAccion(int *elementoPulsado, int *tipoElementoPulsado) { switch(accion){ case ACCION_IR: Fernando Garcia Bernal 145 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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); } // Obtiene el camino desde el personaje // hasta la entidad seleccionada vector<int> camino = devuelveCamino( situacionPersonaje() , elementosSeleccionables [*elementoPulsado] -> situacionEntidad()); this->camino = camino; entidadSeleccionada = *elementoPulsado; // cambia el estado para desplazarse a la // entidad seleccionada estado = ESTADO_DESPLAZAMIENTO; } // si la acción es ir y se ha pulsado el // suelo, entonces se deja de iluminar todo // se calcula el camino hasta ese punto del // suelo y se cambia el estado para desplazarse else if(*tipoElementoPulsado == SELECCION_SUELO){ 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; } } Fernando Garcia Bernal 146 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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--; } if(seleccionInventario < -1) { seleccionInventario = inventario.size()-1; } else if(seleccionInventario >= (signed int)inventario.size()) { seleccionInventario = -1; } // si la acción es usar y se ha pulsado una // entidad, se ilumina, calcula el camino Fernando Garcia Bernal 147 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // y se desplaza hacia ella if(*tipoElementoPulsado == SELECCION_ENTIDAD && *elementoPulsado != -1) { 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; } } Si la acción es ACCION_IR, entonces primero se comprueba si se ha seleccionado algo con el puntero, en tal caso si además lo seleccionado es de tipo Entidad (podría ser suelo) se ilumina el contorno de la entidad con un borde blanco y se obtiene el camino desde la situación actual del personaje hacia el objeto. Finalmente se transita al estado ESTADO_DESPLAZAMIENTO para desplazarse hacia ella. En cambio, si la selección ha sido sobre un elemento suelo, la actuación es similar sólo cambia que el camino se encuentra desde la posición actual hasta la zona de suelo, y en este caso no se ilumina nada. En el punto siguiente se explica cómo calcular la posición del personaje y cómo encontrar el camino. Cuando la acción es ACCION_RECOGER, se comprueba que se ha seleccionado un elemento entidad, se ilumina y se calcula el camino hacia este. El estado también transita a desplazamiento para que se desplace. La última acción posible es ACCION_USAR, y lo primero que hace es actualizar la selección del inventario en función de los botones pulsados. Esto de recorrer se hace solamente aquí ya que es algo exclusivo de esta acción. Después se comprueba si se ha seleccionado alguna Fernando Garcia Bernal 148 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación entidad para como hemos hecho hasta ahora, iluminarla, encontrar el camino y cambiar el estado. El siguiente método que se invoca desde actualizar es actualizarEstado. Los posibles estados son: ESTADO_PARADO y ESTADO_DESPLAZAMIENTO. Si está seleccionado el primero, no se realizará ninguna acción. En cambio el segundo hará que el personaje se desplace hasta su destino. Lo siguiente que se muestra es la implementación del método: bool Escenario::actualizarEstado(int *elementoPulsado, int *tipoElementoPulsado) { bool finEscenario = false; switch(estado) { case ESTADO_PARADO : break; case ESTADO_DESPLAZAMIENTO : // ESTADO_DESPLAZAMIENTO se centra // en ir recorriendo el camino hasta // el destino // obtiene el area de destino int areaDest = camino.at(camino.size()-1); // si ha llegado a su destino realiza // las acciones oportunas según la acción // que lo había movido a desplazarse if(situacionPersonaje() == areaDest){ // inicializa el camino // ya recorrido camino = vector<int>(); // el protagonista se detiene // y por tanto vuelve al frame original Protagonista.fr=0; // si la acción era recoger la recoge si puede, en // tal caso quitándola del inventario if(accion == ACCION_RECOGER && entidadSeleccionada != -1) { Entidad ent = *elementosSeleccionables[entidadSeleccionada]; if(!ent.puedeRecogerse){ sprintf(mensajePantalla, ent.mensajeNoRecoger); Fernando Garcia Bernal 149 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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); } } // si la acción era usar y se ha seleccionado una // entidad entonces se usa la entidad y con un // elemento del inventario si este estuviera // seleccionado else if(accion == ACCION_USAR && entidadSeleccionada != -1) { Entidad *entInventario = NULL; Entidad *ent = elementosSeleccionables[entidadSeleccionada]; if(seleccionInventario != -1) { entInventario = &inventario[seleccionInventario]; } finEscenario = usar(ent, entInventario); } entidadSeleccionada = -1; // es necesario este control ya que al recoger o al // usar, puede ponerse el estado de animacion y en // tal caso no puede interrumpirse if(estado != ESTADO_ANIMACION) estado = ESTADO_PARADO; } // si no ha llegado a su destino, sigue desplazándose // por el camino ya calculado else{ // avanza un frame en la animación del protagonista Protagonista.fr= (Protagonista.fr+1)%Protagonista.nFrames; // siguiente área a la que desplazarse Fernando Garcia Bernal 150 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación int sigArea = camino.at(1); // vectores origen y siguiente área vector2 orig(Protagonista.instante.pos[0], Protagonista.instante.pos[2]); vector2 sig = devuelveCentroArea(sigArea); // distancia a recorrer float distancia = orig.distancia(sig); float distX = orig.x-sig.x, distY = orig.y-sig.y; // desplazamiento en X e Y en función de la variable // velocidadDesplazamiento float despX = distX/(floorf(distancia/velocidadDesplazamiento)+1); float despY = distY/(floorf(distancia/velocidadDesplazamiento)+1); // si la distancia es pequeña, el personaje // llega a su destino if (fabs(distancia) < 0.01f) { Protagonista.instante.pos[0] = sig.x; Protagonista.instante.pos[2] = sig.y; camino.erase(camino.begin(),camino.begin()+1); } else{ // alguno que debe girarse el protagonista // en dirección y sentido al desplazamiento float angulo = asin(distY/distancia)*180.0f/PI+270.0f; if(distX>=.0f){ angulo*=-1.f; } Protagonista.instante.rot[1] = angulo; // cantidad de desplazamiento que debe // trasladarse el protagonista en el eje X float cantidadDesplazamientoX = (fabs(distX)<fabs(despX) || (distY == 0.0f && fabs(distX)<velocidadDesplazamiento)) ? distX : despX; Protagonista.instante.pos[0] -= cantidadDesplazamientoX; // se utiliza el desplazamiento en X para // mover la cámara anclada al usuario cam[0][0] -= cantidadDesplazamientoX; cam[1][0] -= cantidadDesplazamientoX; // cantidad de desplazamiento que debe // trasladarse el protagonista en el eje Y Protagonista.instante.pos[2] -= (fabs(distY)<fabs(despY) || (distX == 0.0f && fabs(distY)<velocidadDesplazamiento)) ? distY : despY; } Fernando Garcia Bernal 151 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación } break; } return finEscenario; } Cuando el estado actual es desplazarse, lo primero que se comprueba es si el personaje ha alcanzado su destino, en tal caso pasa a desempeñar la acción que había desembocado en el desplazamiento. Si se trataba de la acción ACCION_RECOGER, entonces se consulta si la entidad en cuestión permite recogerse a través de su propiedad puedeRecogerse. En caso negativo se muestra un mensaje al usuario para indicárselo. Si se puede recoger, entonces se suma la entidad al inventario del protagonista, se desvincula la entidad del escenario para que deje de mostrarse, y se llama al método virtual recoger. Al igual que sucedía con el método virtual eventoFinalizarAnimacion que se comentó anteriormente, este método no tiene sentido que se implemente en esta clase Escenario, ya que los eventos que puedan suceder al recoger cierta entidad dependerán del escenario concreto que se defina en la historia. Otra posibilidad es que la acción activa sea ACCION_USAR, en este caso se comprueba si el usuario tiene algún elemento del inventario seleccionado para usar sobre el elemento seleccionado con el puntero. Después se llama al método virtual usar que sucede similar a recoger. Cuando se llegue al punto 6.2.7 se verá como implementar clases especializadas que implementan estos métodos. La otra situación es cuando el protagonista no ha terminado de desplazarse, entonces se deben realizar unos cálculos para que continúe su trayectoria. Se incrementa el frame actual del protagonista para avanzar en la animación de movimiento se obtiene de la variable camino la siguiente área a la cual se tiene que desplazar. Se crean dos variables que se corresponden con las coordenadas de origen y destino para poder realizar los cálculos geométricos de desplazamiento. Haciendo uso de trigonometría y conociendo la distancia que queremos desplazarnos, velocidadDesplazamiento, se calcula el desplazamiento que debe hacerse en despX y despY. Se gira la figura en la dirección y sentido del desplazamiento accediendo a su variable instante que almacena la posición, rotación y escalado. Después se traslada la cantidad total que le queda, distX, o la proporción necesaria, despX, en función de si le queda poco por moverse. Finalmente se desplaza la cámara dicha cantidad Fernando Garcia Bernal 152 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. La siguiente llamada es actualizarTextos y se encarga de mostrar los mensajes al usuario sobre la situación del juego. Como en otros casos, también se divide según la acción actual para dar el mensaje correspondiente. A continuación la implementación: void Escenario::actualizarTextos(int *elementoPulsado, int *tipoElementoPulsado) { // si se puede cambiar el mensaje en pantalla... if(tiempoMensajePantalla == -1){ switch(accion){ case ACCION_IR : // Se muestra "Ir a (elemento seleccionado)" 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 : // se muestra "Recoger (elemento seleccionado)" PA_SetSpriteXY(1,0,171,101); sprintf(mensajePantalla, "Recoger"); if(entidadSeleccionada!=-1) { sprintf(mensajePantalla, "%s %s", mensajePantalla, elementosSeleccionables [entidadSeleccionada]->nombre); } break; case ACCION_USAR : // se colorea la selección del inventario // y se muestra "Usar (elemento seleccionado) // con (elemento inventario)" Fernando Garcia Bernal 153 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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--; } Fernando Garcia Bernal 154 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // se pintan los elementos que contiene // en el inventario int x = 3; for(unsigned int i=0; i<inventario.size(); i++) { x = (accion == ACCION_USAR && seleccionInventario == (signed int)i)?6:3; PA_OutputText(1,x,i*3+6,"%s",inventario[i].nombre); } // 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 Fernando Garcia Bernal 155 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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 la entidad suelo Suelo.pintarGL(); // pinta al protagonista Protagonista.pintarGL(); // pinta los elementos no seleccionables for(unsigned int i = 0; i<elementosNoSeleccionables.size(); i++) { elementosNoSeleccionables[i].pintarGL(); } // pinta los elementos seleccionables for(unsigned int i = 0; i<elementosSeleccionables.size(); i++) { elementosSeleccionables[i]->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); Fernando Garcia Bernal 156 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // Establece la vista a la pantalla completa glViewPort(0,0,255,191); // inicializa glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(30, 256.0 / 192.0, 0.1, 20); glMatrixMode(GL_MODELVIEW); // ilumina la entidad si está activo el flag if(iluminar) { glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(1)); } else { glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(0)); } // se pinta cada figura que compone la entidad for(int i=0; i<nFiguras; i++){ 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]); // se calculan a partir del ángulo las // coordenadas (x,y,z) de giro 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; // para evitar división por cero if (s < 0.001f) { x=q.m_x; y=q.m_y; z=q.m_z; } else { x=q.m_x/s; y=q.m_y/s; z=q.m_z/s; } // se actualiza el instante actual glTranslatef( instante.pos[0], instante.pos[1], instante.pos[2]); glRotateX(instante.rot[0]); glRotateY(instante.rot[1]); glRotateZ(instante.rot[2]); glScalef( instante.esc[0], Fernando Garcia Bernal 157 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación instante.esc[1], instante.esc[2]); // se aplican las transformación de la animación if(f[i].nFrames>0){ //Transformaciones del modelo glTranslatef( f[i].a[fr].pos[0], f[i].a[fr].pos[1], f[i].a[fr].pos[2]); glRotatef(angle,x,y,z); glScalef( f[i].a[fr].sca[0], f[i].a[fr].sca[2], f[i].a[fr].sca[2]); } // se recuperan los vértices vert *vaux=(vert *)f[i].v; int nMat=0, vecMat=0; for(int mat=0; mat<f[i].nMateriales; mat++) { nMat=f[i].numeroMat[mat]; vecMat=f[i].vecesMat[mat]; // se vincula la textura correspondiente vincularTextura(nMat); glBegin(GL_TRIANGLE); int c=0; for(int vC=0; vC<vecMat; vC++) { // se pinta cada vértice y su texura con // las primitivas de openGL for(int ve=0; ve<3; ve++){ glTexCoord2f( vaux[c][ve][1], vaux[c][ve][0]); glVertex3f( vaux[c][ve][2], vaux[c][ve][3], vaux[c][ve][4]); } c++; } glEnd(); } } glPopMatrix(1); } } Fernando Garcia Bernal 158 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. 6.2.6.3 Búsqueda de camino En este apartado se explica las técnicas usadas para realizar la búsqueda de caminos entre dos puntos del escenario. La primera llamada que se hace para obtener un camino es al método devuelveCamino. Aquí lo único que se hace es comenzar el camino con la cara de inicio pasada por la variable ini, y después llamar al algoritmo oportuno para obtener el camino. Esta es la implementación del método: vector<int> Escenario::devuelveCamino(int ini, int dest) { vector<int> camino; // se establece el punto de inicio como // el inicio del camino camino.push_back(ini); buscarCaminoAEstrella_ModificadoOptimo(ini, dest, &camino); 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. El algoritmo A* busca en un espacio de estados la ruta de menor coste desde un nodo de comienzo hasta un nodo objetivo examinando los nodos adyacentes. Durante la ejecución, el Fernando Garcia Bernal 159 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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: /** Estructura de un nodo usada para el algoritmo A* */ typedef struct TNodo{ // número del nodo en carasDesplazables int loc; Fernando Garcia Bernal 160 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // índice de la posición en carasDesplazables int indice; // coste desde el inicio hasta este nodo float costFromStart; // coste desde este nodo al destino float costToGoal; // coste total float totalCost; // nodo padre TNodo *parent; } nodo; 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: void Escenario::buscarCaminoAEstrella_ModificadoOptimo(int startLoc, int goalLoc, vector<int>* fin) { // lista de nodos abiertos y cerrados, sin // explorar y explorados respectivamente vector<int> open, close; // aquí se almacenan todos los nodos disponibles // en carasDesplazables vector<TNodo*> nodos; // 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); Fernando Garcia Bernal 161 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación startNode->costFromStart=0; startNode->costToGoal=PathCostEstimate(startLoc,goalLoc); startNode->parent=NULL; open.push_back(startNode->loc); // mientras open no esté vacía se explora // todo el espacio de estados while(!open.empty()) { // recoge el nodo más prometedor de la lista // ordenada open y luego lo saca de ella nodo *node=encuentraNodoPorId(&nodos, open.back()); open.pop_back(); nodo *newNode; // itera por cada uno de los nodos contiguos de node for(unsigned int i=0; i<cont[node->indice].size(); i++) { newNode=encuentraNodoPorId(&nodos, cont[node->indice][i]); // calcula el nuevo coste float newCost=node->costFromStart + PathCostEstimate(node->loc,newNode->loc); // si no está en open y close Y el coste del nodo // examinado es menor que el nuevo coste // entonces se actualiza, sino se ignora if( !( ((esta(newNode->loc,open)!=-1) || (esta(newNode->loc,close)!=-1)) && newNode->costFromStart <= newCost ) ) { // se actualiza su información newNode->parent=node; newNode->costFromStart=newCost; newNode->costToGoal= PathCostEstimate(newNode->loc,goalLoc); newNode->totalCost= newNode->costFromStart + newNode->costToGoal; // variable auxiliar para conocer la posición // del nodo en la lista // y poder eliminarlo int pos; // si newNode está en close se elimina if((pos=esta(newNode->loc,close))!=-1) { close.erase(close.begin() + pos,close.end()-(close.size()-pos)); } // si newNode está en open se inserta en orden if((pos=esta(newNode->loc,open))!=-1) Fernando Garcia Bernal 162 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación { insertar_en_orden(&nodos,newNode,&open); } // sino se inserta en la cabeza else { open.push_back(newNode->loc); } } } //for // una vez examinado, se inserta en close close.push_back(node->loc); }//while // cuando ya ha terminado de examinar el // espacio de estados completo, se recupera // el camino más corto 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); // se da la vuelva a la lista obtenida // y se almacena en la variable fin vector<int>::reverse_iterator rit; for(rit=f.rbegin() ; rit < f.rend(); rit++) { fin->push_back(*rit); } return; } 6.2.7 Crear nuestra propia aventura. Especializaciones de escenario y entidad Se ha estudiado como crear el esqueleto completo que requiere esta aplicación, sin embargo en ningún momento se ha hablado de aspectos concretos de la historia que habíamos planteado en capítulos anteriores. Esto es así porque este proyecto está pensado para que cualquier otra persona pueda reutilizar y definir su propia historia, en base al motor que aquí se ha desarrollado. Fernando Garcia Bernal 163 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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. 6.2.7.1 Herencia de Entidad Cada una de las entidades que tengan que aparecer en el transcurso del juego debe tener una clase propia que herede de Entidad. En esta clase se define cual es el modelo 3D que le corresponde, su textura, si se puede recoger o usar y los mensajes que pueden mostrar. A continuación se muestra de manera genérica cómo debe ser la implementación de una Entidad para un juego, en un fichero .h: #ifndef ENTIDAD_NOMBRE_H #define ENTIDAD_NOMBRE_H #include "Entidad.h" #include "modelo3D.h" #include "textura.h" class NombreEntidad : public Entidad { public: // Se define el constructo pasando como parámetros // el nombre del modelo 3D en formato X, el fichero // que se corresponde con la textura y el tamaño de la // misma que puede ser 64 o 128 NombreEntidad():Entidad(modelo3D, textura, tamañoTextura){ // // // // (Opcional) Por defecto una entidad no puede recogerse, si se activa esta opción entonces si podrá ser recogida por el protagonista puedeRecogerse = true; // // // // (Opcional) Por defecto una entidad no puede usarse, si se activa esta opción entonces si podrá ser usada por el protagonista puedeUsarse = true; // // // // // // (Opcional) Por defecto existen mensajes predefinidos para cuando una Entidad no puede recogerse, si se define aquí un un mensaje, será mostrado cuando esto suceda en lugar del mensaje por defecto sprintf(mensajeNoRecoger,"no se puede recoger"); // (Opcional) Por defecto existen mensajes // predefinidos para cuando una Entidad Fernando Garcia Bernal 164 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS // // // // Implementación no puede usarse, si se define aquí un un mensaje, será mostrado cuando esto suceda en lugar del mensaje por defecto sprintf(mensajeNoUsar,"no se puede usar"); }; ~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. 6.2.7.2 Herencia de Escenario Como sucede en la clase Escenario, en su clase heredada se deberá definir la lógica del juego puesto que sigue siendo esta clase la que relacione a todas las demás teniendo visibilidad sobre estas. En este caso a diferencia de la herencia de entidades, se puede implementar en dos ficheros distintos debido a su mayor magnitud, para tenerlo así un poco más ordenado. La implementación del fichero .h genérica es: #ifndef ESCENARIO_NOMBRE_H #define ESCENARIO_NOMBRE_H #include "Escenario.h" class NombreEscenario : public Escenario { public: NombreEscenario (Personaje *_Prota); Fernando Garcia Bernal 165 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación virtual ~NombreEscenario(){}; // implementa los métodos específicos // recoger, usar y eventoFinalizarAnimacion // para este escenario bool recoger(Entidad *obj1); bool usar(Entidad *obj1, Entidad *obj2); bool eventoFinalizarAnimacion(Entidad *obj1); // define las variables para el personaje y para las entidades // de este escenario Personaje *Prota; Entidad *EntidadHeredada1,*EntidadHeredada2,..*EntidadHeredadan; }; #endif En este fichero es también necesario utilizar un nombre en ESCENARIO_HABITACION_H para utilizar las macros #ifndef y #define, y evitar problemas al incluir estos ficheros. Después se especifica un nombre a esta clase en NombreEscenario. Por último, para tener unas referencias a las entidades que se incluyan dentro de este escenario, se declaran como punteros en EntidadHeredada. El otro fichero es el .cpp donde se implementan los métodos recoger, usar y eventoFinalizarAnimacion. Este es algo más extenso así que iremos viéndolo por métodos. El primero a implementar se trata del constructor: NombreEscenario::NombreEscenario(Personaje *_Prota): Escenario(objetoSuelo, texturaSuelo, tamañoTexturaSuelo, inclinación, distancia, altura) { // ascocia al protagonista Prota = _Prota; // asocia el resto de entidades creadas para este escenario EntidadHeredada1 = new EntidadHeredada1(); EntidadHeredada2 = new EntidadHeredada2(); … EntidadHeredadan = new EntidadHeredadan(); // vincula el objeto Personaje con este escenario vincularProtagonista(Prota,.0f,.0f,.0f); // vincula los elementos seleccionables con este escenario Fernando Garcia Bernal 166 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación vincularElementoSeleccionable(EntidadHeredada1, posiciónX1, posiciónY1, posiciónZ1); vincularElementoSeleccionable(EntidadHeredada2, posiciónX2, posiciónY2, posiciónZ2); … vincularElementoSeleccionable(EntidadHeredadan, posiciónXn, posiciónYn, posiciónZn); // vincula los elementos no seleccionables con este escenario vincularElementoNoSeleccionable(EntidadHeredada1, posiciónX1, posiciónY1, posiciónZ1); vincularElementoNoSeleccionable(EntidadHeredada2, posiciónX2, posiciónY2, posiciónZ2); … vincularElementoNoSeleccionable(EntidadHeredadan, posiciónXn, posiciónYn, posiciónZn); // Lo último es almacenar en el vector mensajes // los mensajes que se deben mostrar al usuario // en las transciciones entre los escenarios mensajes.push_back("mensaje1"); mensajes.push_back("mensaje2"); … mensajes.push_back("mensajen"); } 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 Fernando Garcia Bernal 167 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación más tarde se accede indicando qué mensajes se quieren mostrar. El siguiente método a implementar por esta clase es recoger: bool NombreEscenario::recoger(Entidad *obj1) { // inicialización // esta variable indica si se debe mostrar un mensaje // y cuál debería ser. Las diferentes posibilidades // son: // MSJ_NO_MOSTRAR: no muestra ningún mensaje después de // esta actuación // MSJ_RECOGER_GENERICO: muestra el mensaje genérico definido // al final de este método // MSJ_RECOGER_OBJETO_ESCENA: muestra el mensaje que se define // de manera específica para el objeto a recoger int mostrarMensaje = MSJ_RECOGER_GENERICO; // variable que indica si recoger el objeto hace finalizar el // escenario bool fin = false; // aquí se identifica la entidad que se pretende // recoger y se actúa en consecuencia realizando // las actuaciones adecuadas if(strcmp(obj1->nombre, "nombreEntidadHeredada1") == 0) { // posibles actuaciones: // -> la entidad nombreEntidadHeredadai1 deja // de poder usarse // nombreEntidadHeredadai1->puedeUsarse = false; // -> la entidad nombreEntidadHeredadai2 pasa a // poder usarse // nombreEntidadHeredadai2->puedeUsarse = true; // -> la entidad nombreEntidadHeredadai3 deja // de poder recogerse // nombreEntidadHeredadai3->puedeRecogerse = false; // -> la entidad nombreEntidadHeredadai4 pasa a // poder recogerse // nombreEntidadHeredadai4-> puedeRecogerse = true; // -> no se muestra ningún mensaje // mostrarMensaje = MSJ_NO_MOSTRAR; // -> se muestra el mensaje por defecto // mostrarMensaje = MSJ_RECOGER_GENERICO; // -> se muestra el mensaje específico del objeto // mostrarMensaje = MSJ_RECOGER_OBJETO_ESCENA; // -> recoger esta entidad indica finalizar este escenario // fin = true } else if(strcmp(obj1->nombre, "nombreEntidadHeredada2") == 0) { // realizar actuaciones pertinentes } // aquí se declara el mensaje a mostrar al usuario // en función de la variable mostrarMensaje switch(mostrarMensaje) { case MSJ_RECOGER_GENERICO: Fernando Garcia Bernal 168 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación sprintf(mensajePantalla, "no se puede recoger"); break; 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. Otro método importante para definir un escenario específico es usar. Su comportamiento es similar a recoger aunque ahora pueden intervenir dos objetos diferentes y eso conlleva algunas particularidades. La implementación es la siguiente: bool NombreEscenario::usar(Entidad *obj1, Entidad *obj2) { // La entidad obj1 se identifica con el objeto que se // encuentra en el escenario, mientras que obj2 (si existe) // es el objeto que tiene el usuario en su inventario // inicialización // esta variable indica si se debe mostrar un mensaje // y cuál debería ser. Las diferentes posibilidades Fernando Garcia Bernal 169 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementació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; // aquí se identifica la entidad de la escena // que se pretende usar y se actúa en consecuencia realizando // las actuaciones adecuadas if(strcmp(obj1->nombre, "nombreEntidadHeredada1") == 0) { // es posible que la actuación de usar el objeto // de la escena sólo sea factible si se ha combinado // con un objeto que deba poseer el protagonista // en su inventario // // // // // // // // // // // // // // // 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; // iniciar animación nombreEntidadHeredadai6 { entidadEnAnimacion = nombreEntidadHeredadai6; entidadEnAnimacion->fr = 0; finAnimacion = entidadEnAnimacion->nFrames; estado = ESTADO_ANIMACION; } Fernando Garcia Bernal 170 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación } else if(!nombreEntidadHeredadai6->puedeUsarse) { mostrarMensaje = MSJ_USAR_GENERICO; } else { mostrarMensaje = MSJ_USAR_OBJETO_ESCENA; } } else { mostrarMensaje = MSJ_USAR_GENERICO; } // aquí se declara el mensaje a mostrar al usuario // en función de la variable mostrarMensaje switch(mostrarMensaje) { case MSJ_USAR_GENERICO: sprintf(mensajePantalla, "no se puede usar"); break; 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 Fernando Garcia Bernal 171 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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: bool NombreEscenario::eventoFinalizarAnimacion(Entidad *obj1) { // variable que indica si al finalizar la animación del objeto // hace finalizar el escenario bool fin = false; // aquí se identifica la entidad que se ha finalizado su // animación y se actúa en consecuencia realizando // las actuaciones adecuadas if(strcmp(obj1->nombre, "nombreEntidadHeredada1") == 0) { // actuaciones posibles. Ver actuaciones posibles del // método recoger } // Se usan las variables finAnimacion, que es la variable que // indica el último frame de la animación, y el estado, Fernando Garcia Bernal 172 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // para que el método sepa a que estado debe transitar la // aplicación por defecto para dar por finalizada la animación, // finAnimacion debe ponerse a -1 y estado a ESTADO_PARADO. finAnimacion = -1; estado = ESTADO_PARADO; 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. 6.2.8 Fichero main El último fichero necesario para finalizar el esquema global de este desarrollo, es el encargado de inicializar las estructuras necesarias y arrancar la aplicación completa. Para ello se define un fichero main como punto inicial de la aplicación. Una implementación genérica es: int main() { // inicializa las librerías PAlib // y la interfaz del usuario que // representa el menú inicializarMenu(); // inicializa la escena 3D inicializaEscena3D(); // Creación de los punteros a las variables necesarias // La variable protagonista se reutiliza para ambos // escenarios Personaje *Prota = new Personaje(prota_centrado, protaText); Escenario *Escena1 = new NombreEscenario1(Prota); Escenario *Escena2 = new NombreEscenario2(Prota); … Escenario *Escenan = new NombreEscenarion(Prota); // texto con mensaje inicial Escena1->mostrarTextoPantalla(0,4); while(!Escena1->actualizar()) Fernando Garcia Bernal 173 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación { PA_WaitForVBL(); } Escena1->mostrarTextoPantalla(5,7); reiniciarEscena(); while (!Escena2->actualizar()) { PA_WaitForVBL(); } Escena2->mostrarTextoPantalla(0, 2); return 0; } Las primeras llamadas son necesarias para inicializar correctamente el sistema: inicializarMenu para cargar las librerías de manera correcta y la interfaz de usuario; e inicializaEscena3D carga las componentes 3D de la librería PAlib. Después siempre tiene que existir una variable de Personaje que será vinculado al conjunto de escenario que se definan. Después de crear estos pasándoles el objeto del protagonista, ya comienza la ejecución de los escenarios. Lo normal puede ser que antes de que el usuario tome control del juego, se muestre un texto que lo sitúe en la escena y le pueda explicar los objetivos que debe conseguir. Después el bucle while ejecutará el método ya estudiado actualizar que desencadena todos los acontecimientos para que el usuario se desplace e interactúe con el escenario. Una vez que la historia decida que el escenario llega a su fin (recordar la variable finEscenario booleana que se podía activar para dicha tarea) finaliza este bucle, pudiendo lanzar otro mensaje de información al usuario, para después pasar a otro escenario. Esto se realizará las veces que sean necesarios hasta que se llegue al final del fichero, cuando se terminará la aplicación. 6.3 Implementar nuestra aventura Ya está todo descrito, hasta este punto ya se ha implementado toda la funcionalidad de la que dispone este sistema. Ahora de lo que se trata es de implementar la aventura descrita en los capítulos previos. Seguramente pueda parecer una historia sencilla compuesta por dos escenarios solamente, y puede ser cierto, sin embargo se ha pretendido que la parte a la que se dedique más tiempo es a crear un sistema reutilizable para que un usuario sin tener que tener Fernando Garcia Bernal 174 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación 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: 31 - Vista perspectiva. 32 - Vista frontal. 33 – Textura. 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: Fernando Garcia Bernal 175 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS 34 - Vista perspectiva. • 35 - Vista frontal. Implementación 36 – Textura. EntidadMesa.h: class EntidadMesa : public Entidad { public: EntidadMesa():Entidad(mesa, mesaText, 64){ sprintf(mensajeNoRecoger,"Pesa demasiado"); }; ~EntidadMesa(){}; }; 6.3.2.2 Estatua • Modelo 3D: 37 - Vista perspectiva. • 38 - Vista frontal. 39 – Textura. EntidadEstatua.h: class EntidadEstatua : public Entidad { Fernando Garcia Bernal 176 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación public: EntidadEstatua():Entidad(estatua, estatuaText, 64){ puedeRecogerse = true; }; ~EntidadEstatua(){}; }; 6.3.2.3 Puerta • Modelo 3D: 40 - Vista perspectiva. • 41 - Vista frontal. 42 – Textura. EntidadPuerta.h: class EntidadPuerta : public Entidad { public: EntidadPuerta():Entidad(puerta, puertaText, 64){ }; ~EntidadPuerta(){}; }; 6.3.2.4 Fregona • Modelo 3D: Fernando Garcia Bernal 177 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS 43 - Vista perspectiva. • 44 - Vista frontal. Implementación 45 – Textura. EntidadFregona.h: class EntidadFregona : public Entidad { public: EntidadFregona():Entidad(fregona, fregonaText, 64){ puedeRecogerse = true; }; ~EntidadFregona(){}; }; 6.3.2.5 Llave • Modelo 3D: 46 - Vista perspectiva. • 47 - Vista frontal. 48 – Textura. EntidadLlave.h: Fernando Garcia Bernal 178 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación class EntidadLlave : public Entidad { public: EntidadLlave():Entidad(llave, llaveText, 64){ }; ~EntidadLlave(){}; }; 6.3.2.6 Árbol • Modelo 3D: 49 - Vista perspectiva. • 50 - Vista frontal. 51 – Textura. EntidadArbol.h: class EntidadArbol : public Entidad { public: EntidadArbol():Entidad(arbol, arbolText, 64){ sprintf(mensajeNoRecoger,"No se puede"); }; ~EntidadArbol(){}; }; 6.3.2.7 Coco • Modelo 3D: Fernando Garcia Bernal 179 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS 52 - Vista perspectiva. • 53 - Vista frontal. Implementación 54 – Textura. EntidadCoco.h: class EntidadCoco : public Entidad { public: EntidadCoco():Entidad(coco, cocoText, 64){ }; ~EntidadCoco(){}; }; 6.3.3 Escenarios Pasamos a describir como se han desarrollado los dos escenarios en los que tiene lugar la aventura. 6.3.3.1 Escenario Habitación Este es el primer escenario y donde se encuentran la mayoría de las entidades antes descritas. Está compuesto además por dos entidades adicionales, una que deben incluir todos los escenarios y que se trata del suelo, y otra entidad no seleccionable, es decir que el usuario no puede interactuar con ella, que se corresponde con la pared de la habitación. • Suelo: Fernando Garcia Bernal 180 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS 55 - Vista perspectiva. • 57 – Textura. Pared: 58 - Vista perspectiva. • 56 - Vista frontal. Implementación 59 - Vista frontal. 60 – Textura. EscenarioHabitacion.h: class EscenarioHabitacion : public Escenario { public: EscenarioHabitacion(Personaje *_Prota); virtual ~EscenarioHabitacion(){}; // implementa los métodos específicos // recoger, usar y eventoFinalizarAnimacion // para este escenario bool recoger(Entidad *obj1); bool usar(Entidad *obj1, Entidad *obj2); bool eventoFinalizarAnimacion(Entidad *obj1); // define las variables para el personaje y para las entidades // de este escenario Personaje *Prota; Entidad *Fregona, *Puerta, *Estatua, *Llave, *Mesa, *HabitacionCompleta; }; • EscenarioHabitacion.cpp: Fernando Garcia Bernal 181 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación EscenarioHabitacion::EscenarioHabitacion(Personaje *_Prota):Escenario(suelo, sueloText, 64, 15.0f, -5.f, -1.2f) { // Enlaza al protagonista Prota = _Prota; // Crea las Entidades Fregona = new EntidadFregona(); Puerta = new EntidadPuerta(); Mesa = new EntidadMesa(); Estatua = new EntidadEstatua(); Llave = new EntidadLlave(); HabitacionCompleta = new Entidad(habitacion, habitacionText, 64); // Vincula al protagonista, las entidades seleccionables // y las no seleccionables 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); // crea los mensajes que se mostrarán al usuario // antes y después de la escena // primera tanda de mensajes mensajes.push_back("Te despiertas"); mensajes.push_back("y te encuentras en un cuarto"); mensajes.push_back("que no reconoces."); mensajes.push_back("Debes salir"); mensajes.push_back("porque tienes hambre."); // segunda tanda de mensajes mensajes.push_back("Has podido salir"); mensajes.push_back("...¿donde estas?"); mensajes.push_back("¡cuanta hambre!"); } bool EscenarioHabitacion::recoger(Entidad *obj1) { int mostrarMensaje = MSJ_RECOGER_GENERICO; bool fin = false; // Si el usuario recoge la estatua, entonces Fernando Garcia Bernal 182 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación // al liberar el peso puede desplazar la mesa // que se encuentra debajo if(strcmp(obj1->nombre, "estatua") == 0) { Mesa->puedeUsarse = true; mostrarMensaje = MSJ_NO_MOSTRAR; } switch(mostrarMensaje) { case MSJ_RECOGER_GENERICO: sprintf(mensajePantalla, "no se puede recoger"); break; case MSJ_RECOGER_OBJETO_ESCENA: sprintf(mensajePantalla, obj1->mensajeNoRecoger); break; } if(mostrarMensaje != MSJ_NO_MOSTRAR) { tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA; } return fin; } bool EscenarioHabitacion::usar(Entidad *obj1, Entidad *obj2) { int mostrarMensaje = MSJ_NO_MOSTRAR; bool fin = false; 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 && Fernando Garcia Bernal 183 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación poseeEnInventario("estatua") ) { Llave->puedeRecogerse = true; Mesa->puedeUsarse = false; // inicia animación mesa { entidadEnAnimacion = Mesa; entidadEnAnimacion->fr = 0; finAnimacion = entidadEnAnimacion->nFrames; estado = ESTADO_ANIMACION; } } else if(!Mesa->puedeUsarse) { mostrarMensaje = MSJ_USAR_GENERICO; } else { mostrarMensaje = MSJ_USAR_OBJETO_ESCENA; } } else { mostrarMensaje = MSJ_USAR_GENERICO; } switch(mostrarMensaje) { case MSJ_USAR_GENERICO: sprintf(mensajePantalla, "no se puede usar"); break; 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; } bool EscenarioHabitacion::eventoFinalizarAnimacion(Entidad *obj1) { finAnimacion = -1; estado = ESTADO_PARADO; return false; Fernando Garcia Bernal 184 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación } 6.3.3.2 Escenario Exterior El segundo escenario más simple, contiene dos entidades seleccionables y una no seleccionable. Además consta del suelo como todos los escenarios. • Suelo: 61 - Vista perspectiva. • 62 - Vista frontal. 63 – Textura. EscenarioExterior.h: class EscenarioExterior : public Escenario { public: EscenarioExterior(Personaje *_Prota); virtual ~EscenarioExterior(){}; // implementa los métodos específicos // recoger, usar y eventoFinalizarAnimacion // para este escenario bool recoger(Entidad *obj1); bool usar(Entidad *obj1, Entidad *obj2); bool eventoFinalizarAnimacion(Entidad *obj1); // define las variables para el personaje y para las entidades // de este escenario Personaje *Prota; Entidad *Arbol, *Coco; }; • EscenarioExterior.cpp: EscenarioExterior::EscenarioExterior(Personaje *_Prota):Escenario(suelo3, suelo2Text, 64, 15.0f, -5.f, -1.2f) Fernando Garcia Bernal 185 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación { // Enlaza al protagonista Prota = _Prota; // Crea las Entidades Arbol = new EntidadArbol(); Coco = new EntidadCoco(); // Vincula al protagonista, las entidades seleccionables // y las no seleccionables vincularProtagonista(Prota,.8f,.0f,-.5f); vincularElementoSeleccionable(Arbol,-4.,.0f,.0f); vincularElementoSeleccionable(Coco,-3.8,1.4f,-.3f); // crea los mensajes que se mostrarán al usuario // después de la escena // primera tanda de mensajes mensajes.push_back("Mmm"); mensajes.push_back("ya puedo descansar tranquilo."); mensajes.push_back("FIN"); } bool EscenarioExterior::recoger(Entidad *obj1) { int mostrarMensaje = MSJ_NO_MOSTRAR; bool fin = false; /** * TODO: realizar acciones oportunas al recoger los objetos */ switch(mostrarMensaje) { case MSJ_RECOGER_GENERICO: sprintf(mensajePantalla, "no se puede recoger"); break; case MSJ_RECOGER_OBJETO_ESCENA: sprintf(mensajePantalla, obj1->mensajeNoRecoger); break; } if(mostrarMensaje != MSJ_NO_MOSTRAR) { tiempoMensajePantalla = TIEMPO_MENSAJE_PANTALLA; } return fin; } bool EscenarioExterior::usar(Entidad *obj1, Entidad *obj2) Fernando Garcia Bernal 186 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación { int mostrarMensaje = MSJ_NO_MOSTRAR; bool fin = false; // Si el objeto a user es el árbol, // entonces provoca que se desprenda el // coco que de él cuelga if(strcmp(obj1->nombre, "arbol") == 0) { Arbol->puedeUsarse = false; // inicia animación coco { entidadEnAnimacion = Coco; entidadEnAnimacion->fr = 0; finAnimacion = entidadEnAnimacion->nFrames; estado = ESTADO_ANIMACION; } } switch(mostrarMensaje) { case MSJ_USAR_GENERICO: sprintf(mensajePantalla, "no se puede usar"); break; 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; } bool EscenarioExterior::eventoFinalizarAnimacion(Entidad *obj1) { bool fin = false; // al terminar la animación del coco // se termina el segundo escenario, // y con él la partida if(strcmp(obj1->nombre, "coco") == 0) { // test PA_HideBg(1,3); PA_ClearTextBg(1); Fernando Garcia Bernal 187 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación PA_ShowBg(1,3); // fin test finAnimacion = -1; estado = ESTADO_PARADO; fin = true; } return fin; } 6.3.4 Fichero main Por último, el fichero principal donde se cargan todas las clases y se realizan las llamadas a los bucles de juego. int main() { // inicializa las librerías PAlib // y la interfaz del usuario que // representa el menú inicializarMenu(); // inicializa la escena 3D inicializaEscena3D(); // Creación de los punteros a las variables necesarias // La variable protagonista se reutiliza para ambos // escenarios Personaje *Prota = new Personaje(prota_centrado, protaText); Escenario *Escena = new EscenarioHabitacion(Prota); Escenario *Escena2 = new EscenarioExterior(Prota); // texto con mensaje inicial Escena->mostrarTextoPantalla(0,4); while (!Escena->actualizar()) { PA_WaitForVBL(); } Escena->mostrarTextoPantalla(5,7); reiniciarEscena(); while (!Escena2->actualizar()) { PA_WaitForVBL(); } Fernando Garcia Bernal 188 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Implementación Escena2->mostrarTextoPantalla(0, 2); return 0; } Fernando Garcia Bernal 189 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fernando Garcia Bernal Implementación 190 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Conclusiones 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. 7.1 Inconvenientes y opiniones Una de las características más importantes para llevar a cabo el desarrollo de este tipo de aplicaciones, es que están muy vinculadas a las comunidades que le dan soporte. Es algo que en los capítulos introductorios se comentó como una ventaja, y ciertamente lo es. Seguramente hace unos años sería impensable poder participar en colectivos agrupados por un interés común, como puede ser el desarrollo de aplicaciones para una videoconsola determinada, al estar tan dispersos por el mundo sus colaboradores. Ahora las herramientas que existen en la red (blog, wiki, foro,…) permiten que colectivos de este tipo puedan intercambiar ideas, formación, opiniones e incluso recursos; que permiten facilitar mucho las cosas. En el caso de este proyecto, la comunidad que da soporte a la librería más utilizada en él, PAlib, ha sido determinante gracias a la gran cantidad de documentación que han elaborado y Fernando Garcia Bernal 191 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Conclusiones 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 Fernando Garcia Bernal 192 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Conclusiones 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 Fernando Garcia Bernal 193 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Conclusiones 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. 7.2 Futuros desarrollos Esta aplicación se ha planteado de manera que sea sencillo crear diferentes aventuras, modelando nuevas figuras 3D y creando las clases pertinentes. Sin embargo, quedarían algunos aspectos que pueden aportar mejoras considerables por implementar. • 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. Fernando Garcia Bernal 194 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS • Conclusiones 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]. Fernando Garcia Bernal 195 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Fernando Garcia Bernal Conclusiones 196 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice A. Glosario Apéndice A. Glosario • Software Development Kit (SDK o “devkit”) Acrónimo de software development kit, se trata de un conjunto de aplicación que posibilita a un programador a desarrollar aplicaciones para una plataforma específica: framework, plataforma hardware, videoconsola, sistema operativo,…. Normalmente un SDK incluye una o más API, herramientas de programación y documentación. • 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. Fernando Garcia Bernal 197 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice A. Glosario • 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. • API (Application Program Interface) Acrónimo de Interfaz de Programación de Aplicaciones, es el conjunto de rutinas, protocolos y herramientas para desarrollar aplicaciones software. El objetivo de esta es proveer al desarrollador de una capa de un nivel de abstracción superior que suponga una facilidad para la construcción de la aplicación. • 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. • Fps (Frame por segundo) Acrónimo que indica la frecuencia a la cual un dispositivo que reproduce imágenes, o frames, por cada segundo. Fernando Garcia Bernal 198 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice A. Glosario • 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. • RGB (Red, Green, Blue) El modo de color RGB consiste en la combinación de los colores primarios rojo, verde y azul en distintas proporciones para reproducir el resto de gama de colores. • mapeo UV Es el proceso en el modelador 3D, para hacer que una imagen 2D envuelva o represente a un modelo 3D. Fernando Garcia Bernal 199 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice A. Glosario 64 - Representación del mapeado UV de un cubo. Fernando Garcia Bernal 200 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice B. Contenido del CD-ROM Apéndice B. Contenido del CD-ROM En este apéndice se explica la distribución de carpetas y ficheros que se sigue en el CDROM con el que se entrega esta memoria. • Aplicación: Almacena los recursos relacionados con la aplicación desarrollada a lo largo de esta memoria • proyectoDS: Aplicación principal de este proyecto que consiste en un juego para Nintendo DS. • Arte: Dentro de esta carpeta se encuentran los sprites, modelos 3D y las texturas que se usan en la aplicación. • Por cada escenario se encuentran las siguientes carpetas: • wings: modelos 3D en el formato de wings3D. • texturas: texturas utilizadas en los modelos 3D creados. • 3ds y modelos animados: se incluyen los modelos exportados a 3ds y los modelos con la animación realizada en 3ds Studio. • • X: ficheros que representan los modelos una vez convertidos al formato X. • bin: modelos exportados al formato que debe utilizarse en la aplicación. Código: Aloja el código de la aplicación Fernando Garcia Bernal 201 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice B. Contenido del CD-ROM • Documentación: Documentación de la aplicación generada a partir de los comentarios escritos en el código fuente. • Ejecutables: Incluye los ficheros que se pueden ejecutar en la videoconsola o emuladores. • XtoCPPWin: Aplicación de escritorio que transforma un fichero X en un fichero binario que puede ser cargado como modelo 3D en las aplicación desarrollada para Nintendo DS. • • Código: Aloja el código de la aplicación. • Ejecutable: Aplicación ya compilada lista para usar. 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. • Software: Aplicaciones que se usan para el desarrollo del proyecto. • Conversor Sprites: Herramienta que convierte sprites al formato empleado por las aplicaciones de PAlib. • Conversor Texturas: Convierte las texturas al formato empleado en las aplicaciones gráficas 3D de Nintendo DS. • devkitpro: software que incluye las herramientas necesarias para el desarrollo de aplicaciones para Nintendo DS entre otras videoconsolas. El instalador incluido necesita conectarse a Internet para descargar la aplicación. • Emuladores: Diferentes emuladores mencionados en la memoria que se pueden utilizar para probar las aplicaciones en la máquina de desarrollo. • • DeSmuME • DualiS • iDeaS • NeonDS PAlib Wizard para Visual Studio: Plugin que se instala para que aparezca un asistente de creación de proyectos PAlib en el entorno de desarrollo Visual Studio. • Utilidad para compilar: Conjunto de ficheros que se utiliza para compilar los ficheros fuente y obtener un ejecutable para la videoconsola Nintendo DS. Fernando Garcia Bernal 202 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice B. Contenido del CD-ROM Fernando Garcia Bernal 203 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Apéndice B. Contenido del CD-ROM • Fernando Garcia Bernal 204 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Bibliografía Bibliografía Enlaces [1] “I Máster Universitario en Creación y Desarrollo de Videojuegos” de la Universidad de Málaga http://www.uma.es/estudios/propias/m81401306.html [2] “Máster en Desarrollo de Videojuegos” de la Universidad Complutense de Madrid http://www.fdi.ucm.es/juegos3d/ [3] “Máster en Diseño y Programación de Videojuegos” de la Universidad Europea de Madrid http://www.uem.es/postgrado/master-en-diseo-y-programacion-de-videojuegos-vedicion/presentacion [4] Librería libnds (inglés) http://www.devkitpro.org/ [5] Librería PAlib (inglés) http://palib.com/ [6] Devkitpro (inglés) http://www.devkitpro.org/ [7] Nintendo DS (inglés) http://www.nintendods.com/ Fernando Garcia Bernal 205 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS [8] Bibliografía DSemu (inglés) http://www.ndsemu.com/ [9] Daulis (inglés) http://dualis.1emu.net/ [10] Ideas (inglés) http://www.ideasemu.org/ [11] Alone in de Dark, 1993 (inglés) http://www.adventureclassicgaming.com/index.php/site/reviews/33/ [12] Grim Fandago, 1998 (inglés) http://www.gamespot.com/gamespot/features/all/greatestgames/p-19.html [13] API de la librería PAlib (inglés) http://www.palib.info/Doc/PAlibDoc%20Eng/modules.html [14] Net Framework (inglés) http://www.microsoft.com/downloads/details.aspx?FamilyID=0856eacb-4362-4b0d-8eddaab15c5e04f5&DisplayLang=en [15] No$GBA (inglés) http://nocash.emubase.de/gba.htm [16] Programmers Notepad 2 (inglés) http://www.pnotepad.org/ [17] Visual HAM (inglés) http://www.console-dev.de/visualham.html [18] Visual Studio C++ 2005 Express Edition http://www.microsoft.com/spanish/msdn/vstudio/express/VC/default.mspx [19] Usar PAlib con Visual Studio C++ 200 Express Edition (inglés) http://www.palib.info/wiki/doku.php?id=day1#using_palib_with_visual_c_2005_express [20] DSFTP (inglés) http://giesler.biz/bjoern/en/sw_dsftp.html [21] Adobe Flash http://www.adobe.com/go/BPBIQ [22] Ilustraciones Sandra Arteaga http://www.guiadeilustradores.com/portafolio/portafolio.php?opc=galeria&idper=485&idima=0 [23] UML (inglés) http://www.uml.org/ Fernando Garcia Bernal 206 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS [24] Bibliografía 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/enus/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/ Documentación utilizada ordenada por capítulos • Capítulo 2. Videoconsola Nintendo DS: Referencia técnica no oficial sobre Nintendo DS. http://www.bottledlight.com/ds/index.php/Main/HomePage Hardware para cargar aplicaciones homebrew en Nintendo DS http://en.wikipedia.org/wiki/Nintendo_DS_homebrew http://en.wikipedia.org/wiki/Nintendo_DS_booting_tools http://tobw.net/dswiki/index.php?title=How_to_run_code Desarrollo aplicaciones para Nintendo DS http://forum.gbadev.org/index.php?c=8 http://dev-scene.com/ http://www.warioworld.com/ Fernando Garcia Bernal 207 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS Bibliografía 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 • Capítulo 3. Librería PAlib: Preparación del entorno http://www.palib.info/wiki/doku.php?id=day1#using_palib_with_visual_c_2005_express http://www.microsoft.com/downloads/details.aspx?FamilyID=0856eacb-4362-4b0d-8eddaab15c5e04f5&DisplayLang=en http://giesler.biz/bjoern/en/sw_dsftp.html 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 Programación 3D con openGL y Nintendo DS http://www.palib.info/wiki/doku.php?id=day10 http://delfare.pizz.biz/jour6.htm http://www.opengl.org/documentation http://lazmike.nintendev.com/tutorials/ http://nehe.gamedev.net/lesson.asp?index=01 Fernando Garcia Bernal 208 DESARROLLO DE VIDEOJUEGO 3D PARA LA VIDEOCONSOLA NINTENDO DS <DIRECTORIO DE INSTALACIÓN Bibliografía DEVKITPRO>\examples\nds\Graphics\3D\Misc (*nota: algunos ejemplos dan problemas porque usan elementos obsoletos. Por ejemplo, es necesario cambiar glIdentity por glLoadIdentity) • Capítulo 5. Fase de Diseño: Diagramas de clases UML http://www.agilemodeling.com/artifacts/classDiagram.htm http://www.ibm.com/developerworks/rational/library/content/RationalEdge/sep04/bell/ • 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/enus/dx81_c/directx_cpp/Graphics/Reference/FileFormat/FileFormat.asp http://www.andytather.co.uk/Panda/directxmax_downloads.aspx Quateriones http://msdn2.microsoft.com/eses/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. Fernando Garcia Bernal 209