DESARROLLO DE VIDEOJUEGOS 3D SOFTWARE LIBRE

http://cristiandlr.blogspot.com Cristian de Léon Ronquillo DESARROLLO DE VIDEOJUEGOS 3D CON SOFTWARE LIBRE FACULTAD DE INGENIERÍA DE SISTEMAS, INFORMÁTICA Y CIENCIAS DE LA COMPUTACIÓN QUETZALTENANGO, NOVIEMBRE 2008 i Este proyecto de carrera fue elaborado por el estudiante Cristian Rafael de León Ronquillo como requisito establecido por la Facultad de Ingeniería de la Universidad Mesoamericana de Quetzaltenango, previo a obtener el título de Ingeniero de Sistemas, Informática y Ciencias de la Computación. Quetzaltenango, noviembre de 2008. ii iii ÍNDICE Introducción Propuesta de proyecto Objetivo general Objetivos específicos Capítulo I – Álgebra lineal 1.1 Antecedentes 1.2 Introducción 1.3 Vectores 1.4 Transformaciones lineales Capítulo II – Irrlicht 3D Engine 2.1 Introducción 2.2 Características 2.3 Configurando el IDE (vs2005) y un ejemplo básico 2.4 Cargar mapas de Quake 3 y agregar una cámara FPS 2.5 Manejo de eventos y movimiento 2.6 Graphical User Inrface (GUI) 2.7 Detección de colisiones 2.8 Audio 2D y 3D con irrKlang 2.9 irrXML Capítulo III – RakNet 3.1 Introducción 3.2 Características 3.3 Incluir RakNet en un proyecto 3.4 Comunicación básica: envío de cadenas 3.5 Irrlicht y RakNet, envío de estructuras Capítulo IV – Aplicación: irrArena 4.1 Introducción 4.2 Características del juego 4.3 Instalación del juego 4.4 Diseño del juego Conclusiones Glosario Bibliografía Apéndice A – Licencia de Irrlicht Engine Apéndice B – Contribuciones y recursos Apéndice C – Contenido del CD iv 1 3 5 5 7 9 10 10 17 25 26 26 27 33 37 42 47 53 59 61 63 63 64 65 71 81 83 83 84 91 95 97 101 103 105 107 v INTRODUCCIÓN Los videojuegos representan en la actualidad una de las industrias más grandes del mundo y su popularidad ha venido creciendo de forma paralela a los avances informáticos y tecnológicos con los que se cuentan hoy en día. El presente proyecto de investigación consta de dos partes: Una introducción teórica a los videojuegos 3D y el desarrollo de un videojuego de primera persona. Se inicia desde las bases matemáticas necesarias para trabajar con espacios tridimensionales hasta el uso del Engine de juegos Irrlicht 3D, la API de sonido 3D IrrKlang y la librería de funciones de red RakNet. Con este proyecto de investigación se pretende aportar conocimiento a los estudiantes de ingeniería de sistemas o desarrolladores de software interesados en introducirse al mundo de la programación de videojuegos 3D con software libre. La metodología utilizada para llevar a cabo el presente proyecto es la investigación. Antes de elaborarlo se consultaron diversas fuentes de información relacionadas al tema; se evaluaron características de diversos Engines de videojuegos, tales como Ogre 3D, Blitz3D e Irrlicht 3D; finalmente se tomó la decisión de utilizar Irrlicht 3D por las características que incorpora, la buena estructuración de la API y la amplia documentación con la que cuenta. El presente documento consta de cuatro capítulos, los primeros tres corresponden a la parte teórica del proyecto y cubren los temas de espacios tridimensionales y àlgebra lineal, introducción al Engine Irrlicht 3D, utilización de la API de audio 2D y 3D Irrklang, acceso a archivos de configuración XML por medio de la librería irrXML y utilización de elementos de red por medio de RakNet. La parte demostrativa se detalla en el capítulo cuatro, corresponde al desarrollo de un videojuego de primera persona en red: “irrArena” se abarca no sólo el diseño del mismo, sino también su implementación, utilizando toda la teoría de la primera parte de la investigación. Se espera que este proyecto sea de utilidad a los lectores y que sirva como material de apoyo para futuras investigaciones o proyectos de videojuegos. 1 2 PROPUESTA DE PROYECTO El presente proyecto presenta una investigación sobre algunas tecnologías libres existentes que permiten desarrollar videojuegos 3D, además incluye temas generales acerca de los fundamentos matemáticos de los espacios tridimensionales (Álgebra linal), Irrlicht3D Engine, Manejo de Red con Raknet, acceso a archivos XML por medio de IrrXML, Audio 2D y 3D con IrrKlang y del desarrollo del videojuego de primera persona “irrArena”, aplicando el software antes mencionado. Se compone de cuatro capítulos que explican de forma general y secuencial los fundamentos utilizados para crear un videojuego 3D. Se asume que el lector tiene conocimientos de álgebra y programación, específicamente del lenguaje C++. Los primeros tres capítulos aportan la base teórica y el cuarto capítulo describe el juego irrArena que se incluye en el CD que acompaña a este documento. El capítulo I presenta una introducción a las Matemáticas de los videojuegos. Se tratan temas como sistemas lineales, vectores, interpretación geométrica de las operaciones vectoriales, proyecciones y transformaciones lineales. El capítulo II incluye documentación y ejemplos del Engine Irrlicht 3D, cubriendo temas que van desde el manejador de video, el entorno GUI, manejo de escena, carga de mapas de Quake 3, intercepción de eventos del teclado y del ratón (movimiento), colisiones, iluminación de escena, sonido 2D y 3D con irrKlang y carga de archivos XML con irrXML. En el capítulo III se presenta una introducción a la librería de funciones de Red RakNet, la cual ha sido diseñada específicamente para videojuegos. Se incluyen algunos ejemplos de comunicación de red, tales como el envío de cadenas y una pequeña aplicación que ilustra cómo envíar estructuras de datos genéricos y cómo combinar esta librería con Irrlicht. El capítulo IV es la parte demostrativa del texto. Se desarrolla el videojuego “irrArena”, de primera persona, multijugador, para ello se utiliza Irrlicht3D y Raknet, presentando la documentación correspondiente y el diseño del mismo. 3 4 OBJETIVOS General Presentar una investigación que sirva como un aporte de conocimiento y documentación a los estudiantes de ingeniería y desarrolladores de software interesados en incursionar en mundo de los videojuegos 3D, particularmente con software libre, planteando el presente proyecto de forma demostrativa y práctica. Específicos 1. Investigar las características y el funcionamiento básico de un Engine de gráficos 3D de tiempo real y aplicar este conocimiento para desarrollar un juego de video. 2. Presentar una introducción a la librería de funciones de red RakNet para programar juegos y aplicaciones de red en general. 3. Diseñar y desarrollar el juego de acción “irrArena” para aplicar y exponer el conocimiento adquirido durante la elaboración de este proyecto. 5 6 CAPÍTULO I ÁLGEBRA LINEAL El álgebra lineal es la rama de las matemáticas que estudia conceptos, tales como vectores, matrices, sistemas de ecuaciones lineales y - en un enfoque más formal - espacios vectoriales y transformaciones lineales. En ésta figura se muestra un cubo al que se aplicaron diversas diversas transformaciones de rotación. 7 8 1.1 Antecedentes Un videojuego es un programa informático, creado para el entretenimiento, basado en la interacción entre una o varias personas y un aparato electrónico (ya sea un ordenador, un sistema arcade, una videoconsola o un dispositivo handheld), el cual lo ejecuta. En muchos casos, los videojuegos recrean entornos y situaciones virtuales en los cuales el jugador puede controlar a uno o varios personajes (o cualquier otro elemento), para conseguir ciertos objetivos por medio de reglas determinadas. La popularidad de los videojuegos ha venido aumentando con gran rapidez en las últimas décadas, tanto que su industria representó un valor de 41.9 billones de dólares mundialmente durante el año 2007. Se estima que para 2012 generará unos 68.3 billones de Dólares1, superando así a la industria cinematográfica. En el año de 1961 fue desarrollado Spacewar! – considerado el primer videojuego para computadora – y a partir de esa fecha, la tecnología y la informática han evolucionado para ofrecer nuevas alternativas que mejoran la experiencia del jugador, mostrando entornos más reales. La introducción de Mortal Kombat – un producto que utilizaba Sprites – presentó un avance considerable en la calidad gráfica de los videojuegos. Al operar en dos dimensiones, éste permite manejar un mayor detalle en las animaciones, así como añadir efectos visuales que simulan profundidad, debido a que están basados en mapas de bits dibujados en la pantalla del ordenador. Más adelante, con el videojuego Quake, se introdujeron modelos tridimensionales para los personajes en lugar de utilizar Sprites. En este caso la animación es realizada por medio de bones (huesos virtuales). En la década de 1990 se introdujo el término “Game Engine”, el cual hace referencia a una plataforma que permite manejar los aspectos que son comunes a todos los videojuegos (código reutilizable). Éstos pueden ser: código reutilizable, carga de objetos, visualización y animación de modelos, detección de colisiones entre objetos, física, dispositivos de control como: el teclado, mouse, joystick, etc., interfaces gráficas de usuario e incluso parte de la inteligencia artificial del juego. 1 Study: Video Games on Winning Streak, PC Magazine. http://www.pcmag.com/article2/0,1759,2320607,00.asp 9 1.2 Introducción La Física es una disciplina muy extensa, abarca desde el movimiento simple de un cuerpo, el comportamiento de las ondas mecánicas como el sonido, el comportamiento de la luz y demás ondas electromagnéticas, hasta temas más avanzados como Relatividad y Física Cuántica. Algunos temas de Física son útiles en el desarrollo de videojuegos, por ejemplo, se podría utilizar la óptica para simular la forma en la que la luz viaja e incide sobre los objetos para generar gráficas de alta calidad (Ray-tracing), pero cuando se habla de la Física de los videojuegos se refiere a la mecánica clásica, que consiste en describir el movimiento de sistemas de partículas físicas, de sistemas macroscópicos y a velocidades pequeñas comparadas con la velocidad de la luz. En un videojuego, la mecánica clásica se utiliza para dar a los objetos la sensación de ser sólidos, tener masa, inercia, rebote y cierto grado de capacidad para flotar. Toda la parte física es controlada por el Engine o motor de gráficos 3D, por tanto en este capítulo se hará referencia a conceptos fundamentales del Álgebra lineal, como una introducción a los sistemas lineales, vectores y tranformaciones en el espacio. 1.3 Vectores 1.3.1 SISTEMA LINEAL Muchos problemas pueden modelarse y resolverse por medio de un sistema lineal. Éste consiste en un conjunto de m ecuaciones con n variables (o incógnitas), es decir, un conjunto de m ecuaciones lineales. Las leyes de la mecánica se modelan mediante sistemas lineales. Particularmente interesan los sistemas de tres variables (véase la figura 1.1), donde se puede representar, por ejemplo, la posición en el espacio relativa a un origen de una partícula, mediante las distancias x, y y z trazadas ortogonalmente (a 90o) entre sí. 10 Figura 1.1. Imagen ilustrativa de la posición de una partícula en el espacio respecto al origen. Las flechas que están sobre los ejes representan variables de posición de un sistema lineal de 3 variables (x, y y z) que combinadas denotan la posición de dicha partícula. 1.3.2 VECTORES Algunas medidas físicas como la temperatura, el área, la masa, el volumen y la energía pueden ser representadas simplemente como una magnitud, un número real conocido en álgebra escalar. Sin embargo, cuando se necesita indicar la dirección (además de la magnitud), se utiliza un vector, el cual se define como una magnitud física con dirección y sentido. Por medio de vectores se puede modelar una fuerza, un desplazamiento, una velocidad, una aceleración, etc. Los vectores son objetos algebraicos y geométricos. Este tipo de dualidad permite estudiar la geometría con métodos algebraicos. Un vector es una matriz columna de n x 1 o n-vector, por ejemplo: 1  u =  ,  2  2 v =  4  ,  6   −0.5  −2   w=  3     −1  Los anteriores son vectores 2, 3 y 4 (en R 2 , R 3 y R 4 ) respectivamente. Los elementos de un vector se llaman componentes. 1.3.3 MÓDULO DE UN VECTOR El módulo de un vector es su magnitud y se define de la siguiente manera:  x1  x  2 2 2 v = x1 + x2 + ... + xn , donde v =  2  .  ...     xn  11 En la figura 1.2 se aprecia el significado geométrico de la magnitud de un vector. Figura 1.2. Esta imagen muestra la interpretación geométrica del modulo de un vector, esto es su magnitud desde el origen, denotada por v . 1.3.4 DIRECCIÓN DE UN VECTOR En R 2 la dirección de un vector (véase la figura 1.3) está representada por el ángulo entre el eje x positivo y el vector mismo, mientras que en R 3 la dirección se representa mediante un vector unitario u llamado dirección de v. Figura 1.3. El ángulo θ entre el vector v y el eje x 2 representa la dirección del vector v en R . El vector unitario u se define de la siguiente manera: uv = v v Éste tiene la propiedad de tener magnitud 1 y la misma dirección de v. Existen tres 3 vectores unitarios que se usan como base para generar todo el espacio R , estos son: i, j y k y se definen de la siguiente manera: 12 1  0 0     i =  0  , j = 1  y k = 0  0   0  1  De manera que se puede escribir cualquier otro vector en R 3 como una combinación lineal de estos vectores, por ejemplo: 3 v =  −2  = 3i − 2 j;  0  Otra forma de representar la dirección de un vector en R 3 es mediante sus ángulos directores, tal como se aprecia en la figura 1.4. Figura 1.4. Los ángulos directores son los que van desde un eje hasta el vector v y definen la dirección del mismo. 1.3.5 OPERACIONES VECTORIALES Y SU INTERPRETACIÓN GEOMÉTRICA 1.3.5.a Suma vectorial La suma de vectores consiste en sumar las componentes de los mismos, es decir, la suma de las componentes x será la componente x del vector resultante, y de esta misma manera las componentes y, z, …, se suman para formar las componentes y, z, … del vector resultante, por ejemplo: Sea u = 3i – j, y v = –i +2j – 2k. Hallar u + v u + v = (3 –1)i + (–1 +2)j + (–2)k = 2i + j – 2k; Gráficamente esto es equivalente a colocar, sin importar el orden, (propiedad conmutativa de la suma) el origen de cada vector al final de otro vector, el vector resultante es aquél que se dirige desde el origen del primer vector hasta el final del último, como se aprecia en la figura 1.5. 13 Figura 1.5.. (a) Tres vectores en el espacio (u, v y w). (b) El vector R representa la suma de los vectores u, v y w. 1.3.5.b Resta vectorial Para restar vectores algebraicamente se cambian los signos del vector a restar (sustraendo) y luego se suman ambos vectores. Sea u = 2i – 3j + k, y v = 2i +j – k. Hallar u – v u – v = 2i – 3j + k – (2i +j – k) = –4j; Al restar dos vectores se obtiene un tercer vector dirigido desde el extremo del vector sustraendo hasta el extremo del vector minuendo, como se ve en la figura 1.6. Figura 1.6 6. Interpretación geométrica de la resta de vectores. El vector v – u se dirige desde el extremo de u hasta el extremo de v. 1.3.5.c Producto de un escalar por un vector Para obtener el producto de un vector por un escalar, simplemente se multiplica el escalar por cada uno de los componentes del vector. Esto provoca un aumento (o disminución) proporcional al escalar en la magnitud del vector. Véase V la figura 1.7. 14 Figura 1.7. 1. (a) Un vector u en el espacio. (b) El vector u multiplicado por el escalar c. Cuando ambos factores en el producto son vectores, se pueden realizar dos tipos de producto: uno que genere un escalar, escalar llamado producto escalar (también ( llamado producto interior o producto punto) o bien un producto que genere un nuevo vector, llamado producto vectorial (también llamado producto exterior o producto cruz). 1.3.5.d Producto escalar  a1   b1  a  b  2 2  Sean a = y b =   dos vectores, entonces ntonces el producto escalar de a y b ,  ...   ...       an  bn  denotado por a ⋅ b está dado por: a ⋅ b = a1b1 + a2b2 + ... + an bn Ambos vectores deben tener el mismo número de elementos. Geométricamente se puede interpretar el producto escalar de la siguiente manera: El producto (escalar) de dos vectores no nulos es igual al módulo de uno de ellos por la proyección del otro sobre él. véase la figura f 1.8.) de un vector u sobre otro vector v (diferentes La proyección (véase de cero) se define de la siguiente manera: manera proy v u = 15 u ⋅v v 2 v Figura 1.8. (a) proy v u cuando el ángulo θ entre ambos vectores (u y v) es menor que π 2 . (b) proy v u cuando el ángulo θ entre ambos vectores (u y v) es mayor que π 2 . 1.3.5.e Producto vectorial  a1   b1      Sean u y v dos vectores tales que, u = a2 y v = b2 el producto cruz entre      a3  b3  j i  ambos está definido por la determinante de la matriz a1 a2   b1 b2 i j u × v = a1 b1 a2 b2 k a3  , de forma que: b3  k a3 = (a2b3 − a3b2 )i − (a1b3 − a3b1 ) j + (a1b2 − a2b1 )k; b3 Donde i , j y k son los vectores unitarios (bases de R3 ). El producto cruz está definido únicamente para R 3 y gráficamente u × v es un vector ortogonal, tanto a u como a v (vector normal n ). Hay dos vectores que son ortogonales, tanto a u como a v : u × v y v × u ; la dirección del vector u × v se determina por medio de la regla de la mano derecha2. Si se coloca la mano derecha de manera que el índice apunte en la dirección de u y el dedo medio en la dirección de v , entonces el pulgar apuntará en la dirección de u × v . Véase la figura 1.9. 2 Algebra lineal. Stanley I. Grossman. El producto cruz de dos vectores, pág. 263. Editorial McGraw-Hill, 1996. 16 Figura 1.9. La dirección de u × v se puede determinar usando regla de la mano derecha. Algunas propiedades importantes del producto cruz se enuncian a continuación: Sean • • • • • • u , v y w tres vectores en R3 y sea a un escalar cualquiera, entonces: u × 0 = 0 × u = 0. u × v = −(v × u ). (Propiedad anticonmutativa) ( au ) × v = a (u × v). u × (v + w) = (u × v) + (u × w). (Propiedad distributiva) (u × v) ⋅ w = u ⋅ (v × w). (Triple producto escalar) u ⋅ (u × v) = v ⋅ (u × v) = 0. ( u × v es ortogonal a u y a v ) 1.4 Transformaciones lineales En los gráficos computacionales, las transformaciones lineales sirven para dar la sensación de movimiento en tiempo real a un determinado objeto en el espacio, se puede por ejemplo, cambiar la posición del objeto (traslación), rotar el objeto en torno a un eje, aumentar o disminuir su tamaño (escalar el objeto), sesgar el objeto o bien realizar cualquier combinación de lo anterior, simultáneamente. Figura 1.10. Transformación de una imagen. Una transformación3 T (también llamada mapeo o función) de un conjunto A a un conjunto B , representada por T : A → B , es una regla que asocia a cada elemento de A un elemento b , único de B , llamado imagen de a bajo T . Se escribe T (a) = b y se dice que a se mapea a T ( a ) . A se llama dominio de T . B es codominio de T . El 3 Definición de transformación: Algebra lineal con aplicaciones. Transformaciones lineales, pág. 307. George Nakos y David Joyner. Editorial Thomson, 1999. 17 subconjunto de B formado por todas las imágenes de los elementos de A se llama rango o contradominio de T y se representa por R (T ) . Una transformación se llama lineal cuando cumple con las siguientes características: Sean • • u y v dos vectores y a un escalar, entonces T (u + v) = T (u ) + T (v). T (cu ) = cT (u ). Se puede transformar cualquier punto (un vector en R 3 ) en otro, usando una matriz de transformación (de 4×4 para R 3 , esto es necesario por la transformación de traslación que verá más adelante). Se usan matrices porque permiten modificar cada componente en un vector respecto de los tres ejes del espacio. En el siguiente ejemplo, una matriz reinterpreta el punto (x, y, z), produciendo el nuevo punto (x', y', z'): Simplemente se multiplica el vector (punto a transformar) v = [x y z 1] por la matriz de transformación  M 11 M M =  21  M 31   M 41 M 12 M 13 M 22 M 32 M 42 M 23 M 33 M 43 M 14  M 24  M 34   M 44  así: [x ' y ' z ' 1] = [ x y  M 11 M z 1]  21  M 31   M 41 M 12 M 22 M 32 M 13 M 23 M 33 M 42 M 43 M 14  M 24  M 34   M 44  De forma que: x ' = xM 11 + yM 21 + zM 31 + 1M 41 , y ' = xM 12 + yM 22 + zM 32 + 1M 42 , x ' = xM 13 + yM 23 + zM 33 + 1M 43 . Se pueden combinar las matrices de transformación en una matriz simple para calcular varias transformaciones en una sola operación (esta operación es conocida como 18 concatenación). Esto se consigue multiplicando las matrices individuales transformación (Véase la definición de Skeletal Animation en el glosario). de 1.3.1 EJEMPLOS DE TRANSFORMACIONES LINEALES COMUNES A continuación se examinará brevemente algunas matrices de transformación. 1.3.1.a Escalado Para cambiar el tamaño de un objeto en R 3 se aplica la siguiente transformación (tal como se indicó anteriormente) a cada uno de los vértices que lo conforman:  sx 0  0  0 0 sy 0 0 0 0 sz 0 0 0  0  1 Donde s x , s y y s z son números reales que determinan las escalas de los ejes x, y y z respectivamente. Por ejemplo, si se desea duplicar el tamaño de un triángulo cuyos vértices son [0 0 0] , [1 1 0] y [ 2 0 0] basta con multiplicar cada uno de estos vectores por la 2 0 matriz de transformación  0  0 0 2 0 0 0 0 2 0 0 0 para obtener los vértices del triángulo 0  1 transformado (véase la figura 1.11). 2 0 [0 0 0 1]  0  0 2 0 [1 1 0 1]  0  0 2 0 [ 2 0 0 1]  0  0 0 0 0 2 0 0  = [0 ⋅ 2 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ 0 0 ⋅ 0 + 0 ⋅ 2 + 0 ⋅ 0 + 1 ⋅ 0 0 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 2 + 1 ⋅ 0 0 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅1] = [ 0 0 0 1] 0 2 0  0 0 1 0 0 0 2 0 0  = [1 ⋅ 2 + 1 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ 0 1 ⋅ 0 + 1 ⋅ 2 + 0 ⋅ 0 + 1 ⋅ 0 1 ⋅ 0 + 1 ⋅ 0 + 0 ⋅ 2 + 1 ⋅ 0 1 ⋅ 0 + 1 ⋅ 0 + 0 ⋅ 0 + 1 ⋅1] = [ 2 2 0 1] 0 2 0  0 0 1 0 0 0 2 0 0  = [2 ⋅ 2 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ 0 2 ⋅ 0 + 0 ⋅ 2 + 0 ⋅ 0 + 1 ⋅ 0 2 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 2 + 1 ⋅ 0 2 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅1] = [ 4 0 0 1] 0 2 0  0 0 1 19 Figura 1.11. 1.1 (a) Un triángulo definido por sus vértices. (b) El resultado de aplicar una transformación de tamaño al triángulo del inciso (a) por un factor de 2. 1.3.1.b Rotación Se puede rotar un punto alrededor de un eje, eje para ello se cuenta con tres matrices de rotación, una para cada eje, pueden combinarse (como ya se mencionó) multiplicando las matrices entre sí. Nota: Las matrices de rotación definidas aquí corresponden a un sistema de mano izquierda,, como se aprecia en la ffigura 1.12. Figura 1.12. 1.1 En un sistema de mano izquierda, la dirección positiva del eje z es la que se aleja del observador. En un sistema de mano derecha ocurre lo contrario, la dirección positiva del eje z es la que se acerca al observador. La siguiente transformación rota el punto (x, y, z)) sobre el eje x, produciendo un nuevo punto (x', y', z'): [x' y ' z ' 1] = [ x y 0 1 0 cos θ z 1]  0 − sin θ  0 0 20 0 sin θ cos θ 0 0 0  0  1 La siguiente transformación rota el punto sobre el eje y: [x ' y ' z ' 1] = [ x cos θ  0 z 1]   sin θ   0 y 0 − sin θ 1 0 0 cos θ 0 0 0 0  0  1 La siguiente transformación rota el punto sobre el eje z: [x ' y ' z ' 1] = [ x  cos θ  − sin θ z 1]   0   0 y sin θ cos θ 0 0 0 0 1 0 0 0  0  1 Donde θ simboliza el ángulo de rotación en radianes. Los ángulos se miden en el sentido horario cuando se mira sobre el eje de rotación hacia el origen. 1.3.1.c Traslación La traslación permite –como su nombre lo indica– trasladar todos los vértices que conforman un objeto en el espacio a una nueva posición. El resultado de esta operación es equivalente a sumar Tx , Ty y Tz a las coordenadas obteniendo el vector  x + Tx y + Ty z + Tz punto (x, y, z) a un nuevo punto (x', y', z'). [x ' y ' z ' 1] = [ x x , y y z , respectivamente, 1 . La siguiente transformación traslada el y 1 0 z 1]  0  Tx 0 0 1 0 Ty 0 1 Tz 0 0  0  1  1.3.1.d Sesgado Esta transformación (también conocida como corte o deslizamiento) permite deslizar un punto a través de un eje determinado. La figura 1.10 es un ejemplo de transformación de corte relativo al eje x en 2D. Las figuras 1.13 y 1.14 muestran un cubo en el espacio que ha sido transformado, primero respecto al eje x (figura 1.13) y luego respecto al eje y (figura 1.14). 21 La siguiente transformación desliza el punto (x, ( y, z)) por un factor c en el eje x (plano xy),, produciendo un nuevo punto (x', ( y', z'). [x' y ' z ' 1] = [ x y 1 c z 1]  0  0 0 1 0 0 0 0 1 0 0 0  = [ x + cy 0  1 y z 1] Figura 1.13. 1.1 La figura muestra el efecto que produce un deslizamiento en el eje x (utilizando el plano xy). La siguiente transformación desliza el un punto sobre el eje y (plano xy): [x' y ' z ' 1] = [ x y 1 0 z 1]  0  0 c 1 0 0 0 0 1 0 0 0  = [ x cx + y 0  1 z 1] Figura 1.15. La figura muestra el efecto que produce un deslizamiento en el eje y (utilizando el plano xy). En general, se puede realizar un deslizamiento en cualquier eje utilizando un plano determinado y las transformaciones se pueden expresar como una función de la siguiente manera: Deslizamiento en x, plano xy: xy S x ( x, y , z ) = ( x + cy, y, z ). Deslizamiento en y, plano xy: xy S y ( x, y, z ) = ( x, y + cx, z ). Deslizamiento en x, plano xz: xz S x ( x, y , z ) = ( x + cz , y, z ). Deslizamiento en z, plano xz: xz S z ( x, y , z ) = ( x, y , z + cx ). 22 Deslizamiento en y, plano yz: S y ( x, y, z) = ( x, y + cz, z ). Deslizamiento en z, plano yz: S z ( x, y , z ) = ( x, y, z + cy ). El factor c puede ser tanto positivo como negativo, esto se refleja en la dirección del eje sobre el que se realiza el deslizamiento. 23 24 Capítulo II Irrlicht3D Engine Indoor Rendering es una característica integrada en el Engine que permite utilizar mapas de luz y superficies curvas; adicionalmente existe un detector de colisiones y un sistema de respuesta. 25 2.1 Introducción Irrlicht3D es un motor de gráficos 3D de alto rendimiento y de código abierto escrito en C++ y utilizable tanto en C++ como en Lenguajes .NET. Es completamente independiente de la plataforma, utilizando Direct X, Open GL o su propio software de renderizado y tiene todas las características que pueden ser encontradas en otras plataformas 3D comerciales. 2.2 Características Irrlicht es un Engine 3D de tiempo real, multi-plataforma y de alto desempeño escrito en lenguaje C++. Es una poderosa API4 de alto nivel para la creación de completas aplicaciones 3D y 2D tal como juegos o aplicaciones para visualización científica. Viene con una excelente documentación e integra todas las características actualmente utilizadas para la representación visual como sombras dinámicas, sistemas de partículas, animación de personajes, tecnología interior y exterior (indoor and outdoor technology) y detección de colisiones. Cabe mencionar que este software está actualmente en desarrollo. La información acerca de los avances en el desarrollo del proyecto está disponible en la siguiente página web: http://irrlicht.sourceforge.net/development.html. Sus principales características5 son: • Renderizado 3D en tiempo real de alto desempeño utilizando Direct3D y OpenGL. • Independiente de la plataforma. Compatible con Windows 95, 98, NT, 2000, XP, Vista, GNU/Linux, y MacOS. • Incorpora una enorme y extensible librería de materiales con soporte para Pixel Shaders y Vertex Shaders. • Manejo de escenas altamente personalizable para interiores y exteriores. • Sistema de animación de modelos con esqueletos (Skeletal Animation) y animación de vértices (Morph target animation). • Efectos de partículas, billboards, mapas de luz, mapeo de entorno, stencil buffer shadows y muchos otros efectos especiales. • Bindings para .NET, lo que hace que el Engine esté disponible para cualquier lenguaje de la plataforma .NET tal como C#, VisualBasic y Delphi.NET. • Incluye dos rápidos renderizadores por software independientes, tanto de la plataforma como del driver de video que tienen diferentes propiedades: Corrección de texturas mapeadas en perspectiva, filtrado bilineal, corrección sub-píxel, z-buffer, Gouraud shading, alpha-blending y transparencias, dibujo 2D rápido y más. 4 5 API, del inglés: Aplication Programming Interface o Interfaz de programación de aplicaciones. Fuente: Irrlicht3D Features, http://irrlicht.sourceforge.net/features.html 26 • Sistema de interfaz gráfica de usuario (GUI) personalizable y fácil de usar. Incluye botones de comando, listas, cajas de texto, etc. • Funciones de dibujo en 2D: alpha blending, blitting, dibujo de fuentes y mezcla de gráficas 2D y 3D. • Una API bien documentada y correctamente estructurada. • Escrito completamente en lenguaje C++ y totalmente orientado a objetos. • Funciones para lectura directa de los formatos más comunes de objetos 3D, entre ellos: 3D Studio meshes (.3ds), B3D files (.b3d), Alias Wavefront Maya (.obj), Cartography shop 4 (.csm), COLLADA (.xml, .dae), DeleD (.dmf), FSRad oct (.oct), Irrlicht scenes (.irr), Irrlicht static meshes (.irrmesh), Microsoft DirectX (.x) (binary & text), Milkshape (.ms3d), My3DTools 3 (.my3D), OGRE meshes (.mesh), Pulsar LMTools (.lmts), Quake 3 levels (.bsp), Quake 2 models (.md2) y STL 3D files (.stl). • Funciones de lectura directa de formatos de textura: Adobe Photoshop (.psd), JPEG File Interchange Format (.jpg), Portable Network Graphics (.png), Truevision Targa (.tga), Windows Bitmap (.bmp) y Zsoft Paintbrush (.pcx). • Rápido sistema de detección de colisiones. • Librerías de contenedores de plantillas de funciones matemáticas 3D rápidas y optimizadas. • • • • Lectura directa de archivos comprimidos (.zip). Parser (Analizador sintáctico) de archivos XML integrado (irrXML). Soporte para Unicode. Compatible con Microsoft VisualStudio6.0™, VisualStudio.NET 7.0-8.0™, Metrowerks Codewarrior y Bloodshed Dev-C++ con g++3.2-4.0. • El Engine es de código abierto y completamente libre. Puede ser depurado, corregido e incluso modificado sin la obligación de hacer públicos dichos cambios: El Engine está bajo los términos de la licencia zlib6. 2.3 Configurando el IDE (Visual Studio 2005) y un ejemplo básico Como se ha descrito en la sección 1.2, Irrlicht puede ser incorporado en una amplia variedad de Entornos de Desarrollo Integrados (IDEs), siendo los pasos para la integración similares en los múltiples entornos de desarrollo. Aquí se describe cómo configurarlo con uno de los entornos de desarrollo más utilizados por los desarrolladores de software7; éste es el caso de Microsoft Visual Studio (actualmente puede obtenerse gratuitamente la versión Express en http://www.microsoft.com/Express/). El SDK de Irrlicht puede obtenerse en http://irrlicht.sourceforge.net/downloads.html, la versión actual es la 1.4.2 e incluye la documentación, tutoriales, exportadores y varios 6 7 Véase el Apéndice A. Licencia del Engine Irrlicht3D. Texto Original - Tutorial 1: HelloWorld - http://irrlicht.sourceforge.net/tut001.html 27 programas de demostración, incluyendo el código fuente y archivos necesarios para correr todos los ejemplos que se detallan a continuación. 2.3.1 CONFIGURANDO EL IDE Para utilizar el Engine se debe incluir el archivo de cabecera irrlicht.h. Éste archivo se encuentra en el directorio include del SDK. Para permitir al compilador acceder a dicho archivo, se debe configurar la ruta al mismo. En Microsoft Visual Studio se debe realizar el siguiente procedimiento: 1. Seleccionar Opciones (Options) del menú de Herramientas (Tools) 2. Seleccionar la sección de Proyectos y Soluciones (Projects And Solutions) y buscar la opción de Directorios de VC++ (VC++ Directories) como se vé en la Figura 2.2. Figura 2.2. Cuadro de opciones del menú Herramientas Visual Studio 2005. 3. Seleccionar Include Files del combo que dice Mostrar directorios para (Show directories for) como se muestra en la Figura 2.3. Figura 2.3. Opción “Show directories for” del cuadro de opciones de Visual Studio 2005. . Dicho direcotrio se encuentra en la 4. Agregar el directorio include con el botón carpeta de instalación del SDK. 5. También es necesario agregar la librería irrlicht.lib, para ello se debe seleccionar la opción Archivos de Librería (Library files) del combo Mostrar directorios para (Show directories for) y agregar el directorio lib/VisualStudio incluído en el SDK. 28 2.3.2 HOLA MUNDO ! El siguiente programa ejemplifica los aspectos básicos del Driver de video, el entorno GUI, y el manejo de escena. Lo primero que se debe hacer es incluir el archivo de cabecera irrlicht.h #include En el Engine Irrlicht todo se encuentra dentro del nombre de espacio irr. Para utilizar cualquiera de las clases del Engine se debe escribir irr:: antes del nombre de la clase. Por ejemplo, para acceder a la clase IrrlichtDevice, se debe escribir irr::IrrlichtDevice. Para evitar escribir irr:: antes de cada clase, se indica al compilador que se utilizará ese nombre de espacio de ahora en adelante y no habrá que escribir irr:: nuevamente. using namespace irr; En el Engine Irrlicht hay cinco sub-espacios. Se puede encontrar información detallada de ellos en la documentación: http://irrlicht.sourceforge.net/docu/namespaces.html De la misma forma en la que se declaró el nombre de espacio irr, se declara la utilización de estos cinco nombres de espacio para mantener este ejemplo fácil de entender. using namespace core; using namespace scene; using namespace video; using namespace io; using namespace gui; Para poder utilizar el archivo Irrlicht.DLL, se debe vincular la aplicación con Irrlicht.lib. Ésta vinculación se puede establecer en las configuraciones del proyecto, pero para facilitar las cosas, se utilizará la instrucción pragma comment de Visual Studio. En plataformas Windows, se debe ocultar la ventana de consola, la cual salta cuando se inicia el programa en main(). Esto se hace con la segunda instrucción pragma. También se puede utilizar el método WinMain(), pero de esta manera la aplicación ya no es independiente de la plataforma (sistema operativo). #ifdef _IRR_WINDOWS_ #pragma comment(lib, "Irrlicht.lib") #pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup") #endif 29 Éste es el método main(), el cual es utilizado por cualquier plataforma. int main() { La función más importante del Engine es createDevice(). El Dispositivo Irrlicht es creado al llamarla. Éste es el objeto raíz para hacer cualquier cosa con el Engine. createDevice() tiene siete parámetros: • deviceType: tipo de dispositivo. Puede ser un dispositivo nulo, D3D8, D3D9, OpenGL o bien alguno de los dos renderizadores de software incluídos en el Engine. En este ejemplo se usa el EDT_SOFTWARE, pero esto puede ser sustituido por EDT_BURNINGSVIDEO, EDT_NULL, EDT_DIRECT3D8, EDT_DIRECT3D9, o EDT_OPENGL, según el tipo de dispositivo que se requiera. • windowSize: define el tamaño de la ventana, o bien, la resolución de la pantalla cuando se está ejecutando en modo de pantalla completa. En este ejemplo se usa 640x480. • bits: define la cantidad de bits de color por píxel, 16 ó 32. El parámetro es generalmente ignorado cuando se ejecuta en modo de ventana. • fullscreen: especifica si se va a ejecutar en pantalla completa o en modo de ventana. • stencilbuffer: especifica si se va a utilizar stencil buffer (para dibujar sombras). • vsync: especifica la activación o desactivación de vsync, esto solo es útil en el modo de pantalla completa. • eventReceiver: es un objeto para interceptar eventos. Por ahora no se utiliza, así que se establecrá su valor a 0. Siempre se debe verificar el valor de retorno para determinar controladores no soportados, dimensiones no válidas, etc. IrrlichtDevice *device = #ifdef _IRR_OSX_PLATFORM_ createDevice(video::EDT_OPENGL, dimension2d(640, 480), 16, false, false, false, 0); #else createDevice(video::EDT_SOFTWARE, dimension2d(640, 480), 16, false, false, false, 0); #endif if (!device) return 1; Ahora se establecerá el título de la ventana. Notese que hay una ‘L’ antes de la cadena. El Engine Irrlicht utiliza cadenas de caracteres anchos para desplegar texto. 30 device->setWindowCaption(L"Hello World! - Irrlicht Engine Demo"); Ahora se crea un puntero al driver de video, el manejador de escena y el entorno de interfaz gráfica de usuario, de esta forma no se tendrá que escribir siempre: device>getVideoDriver(), device->getSceneManager(), o device->getGUIEnvironment(). IVideoDriver* driver = device->getVideoDriver(); ISceneManager* smgr = device->getSceneManager(); IGUIEnvironment* guienv = device->getGUIEnvironment(); Se añade una etiqueta a la ventana con el texto Hello World, usando para ello el entorno GUI. El cuadro de texto estático es posicionado en (10, 10) como la esquina superior izquierda y (260, 22) como la esquina inferior derecha. guienv->addStaticText( L"Hello World! This is the Irrlicht Software renderer!", //cadena a desplegar rect(10,10,260,22), //dimensiones de la caja de texto true //borde de la caja de texto ); Para mostrar algo más interesante, se cargará y desplegará en la escena un modelo MD2. Únicamente se debe cargar la malla desde el manejador de escena con getMesh() y agreagar un nodo de escena para desplegar la malla con addAnimatedMeshSceneNode(). Es recomendable verificar el valor de retorno de getMesh() para asegurar que no haya ocurrido ningún problema al abrir el archivo. En lugar de escribir el nombre de archivo sydney.md2, es posible cargar un archivo de objeto de Maya (.obj), un mapa completo de Quake 3 (.bsp) o cualquier otro formato de archivo soportado por el Engine (véase el listado de características para más información al respecto, Sección 2.2). El modelo sydney.md2 fue diseñado por Brian Collins. IAnimatedMesh* mesh = smgr->getMesh("../../media/sydney.md2"); if (!mesh) return 1; IAnimatedMeshSceneNode* node = smgr->addAnimatedMeshSceneNode( mesh ); Se debe cargar un material a la malla para que se vea mucho mejor. También se desactivará la iluminación porque por ahora no se ha agregado ninguna luz dinámica, de otra forma el modelo se vería completamente negro. Luego se establece el frame loop o ciclo de animación al estado predefinido STAND y por último se aplica una textura a la malla. Sin ésta última la malla sería dibujada, utilizando únicamente un color. if (node) { node->setMaterialFlag(EMF_LIGHTING, false); //desactivar iluminación node->setMD2Animation(scene::EMAT_STAND); //frame loop node->setMaterialTexture( //Aplicar la textura a la malla node 0, //capa de la textura driver->getTexture("../../media/sydney.bmp") //archivo de la textura 31 ); } Para hacer la malla visible en la pantalla se agrega una cámara en el espacio 3D, en la posición (0, 30, -40). La cámara verá desde allí hasta la posición (0, 5, 0), que es aproximadamente el lugar donde se cargó el modelo MD2. smgr->addCameraSceneNode(0, vector3df(0,30,-40), vector3df(0,5,0)); El siguiente paso es configurar la escena y dibujar todo: Se ejecuta el dispositivo en un ciclo while(), hasta que el dispositivo no desee continuar. Esto podría ocurrir cuando el usuario cierra la ventana o presiona Alt + F4 (o cualquier otro comando que cierre la ventana). while(device->run()) { Cualquier cosa puede ser dibujada entre una llamada a beginScene() y una a endScene(). La llamada a beginScene() limpia la pantalla con un color determinado y el buffer de profundidad (depth buffer) si así se requiere. Luego se deja al manejador de escena y al entorno GUI dibujar su contenido. Con una llamada a endScene() todo es desplegado en la pantalla. driver->beginScene(true, true, SColor(255,100,101,140)); smgr->drawAll(); guienv->drawAll(); driver->endScene(); } Después que se ha terminado con el ciclo de renderizado, se debe eliminar el dispositivo Irrlicht creado previamente con createDevice(). En el Engine Irrlicht se deben eliminar todos los objetos que se hayan creado con algún método o función que inicie con create. El objeto será eliminado simplemente con una llamada al método ->drop(). Para más información véase el artículo irr::IReferenceCounted::drop() en la documentación del Enigne. device->drop(); return 0; } Solo hace falta compilar y correr la aplicación. A continuación una muestra de la pantalla del programa en ejecución en la figura 2.4. 32 Figura 2.4. Una muestra de la aplicación de ejemplo ‘Hola Mundo’ ejecutándose. 2.4 Cargar mapas de Quake 3 y agregar una cámara FPS En esta sección se explica como cargar desde el Engine un mapa del juego Quake 3, crear un nodo de escena para optimizar la velocidad de renderizado y cómo crear y utilizar una cámara controlada por el usuario (First Person Shooter Camera o Cámera FPS) Se inicia como en la sección anterior, incluyendo el archivo de cabecera del Engine y un archivo adicional, iostream, para permitir al usuario seleccionar desde la consola el driver que se va a utilizar. #include #include Como ya se mencionó en el ejemplo de la sección anterior, en el Engine Irrlicht todo se encuentra dentro del nombre de espacio ‘irr’. Para evitar escribir irr:: antes del nombre de cada clase del Engine se indica al compilador que se utilizará ese nombre de espacio de aquí en adelante. Hay otros cinco nombres de espacio (subepacios) dentro del nombre de espacio irr, estos son: core, scene, video, io y gui. A diferencia del ejemplo anterior, no se declara la utilización de estos nombres de espacio para distinguir las clases contendidas dentro de cada uno de ellos. using namespace irr; De nuevo, para poder utilizar el archivo Irrlicht.DLL, se debe vincular con el archivo Irrlicht.lib. Se utiliza una instrucción pragma comment lib: #ifdef _MSC_VER #pragma comment(lib, "Irrlicht.lib") #endif 33 De nuevo, se utiliza el método main() para iniciar (en lugar de WinMain()). int main() { Ahora se crea el dispositivo con createDevice(). La diferencia ahora es que se consultará al usuario qué controlador desea utilizar. El dispositivo de software podría ser muy lento para dibujar un mapa enorme de Quake 3; para propósitos de prueba se hará de ésta una opción del menú. // Elegir manejador de video video::E_DRIVER_TYPE driverType; printf("Por favor elija un manejador de video para la aplicación:\n"\ " (a) Direct3D 9.0c\n"\ " (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\ " (d) Software Renderer\n"\ " (e) Burning's Software Renderer\n"\ " (f) NullDevice\n (otherKey) exit\n\n" ); char i; std::cin >> i; switch(i) { case 'a': driverType = video::EDT_DIRECT3D9;break; case 'b': driverType = video::EDT_DIRECT3D8;break; case 'c': driverType = video::EDT_OPENGL; break; case 'd': driverType = video::EDT_SOFTWARE; break; case 'e': driverType = video::EDT_BURNINGSVIDEO;break; case 'f': driverType = video::EDT_NULL; break; default: return 1; } // Crear el dispositivo o salir si hay una falla IrrlichtDevice *device = createDevice(driverType, core::dimension2d(640, 480)); if (device == 0) return 1; //No se pudo crear el dispositivo. Se crea un puntero al manejador de video y otro para el manejador de escena, de esta manera no es necesario tener que llamar siempre a irr::IrrlichtDevice::getVideoDriver() y irr::IrrlichtDevice::getSceneManager(). video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager(); Para desplegar el mapa de Quake 3 primero se debe cargar. Los mapas de Quake 3 están empaquetados dentro de archivos .pk3, los cuales no tienen ninguna diferencia con los archivos .zip. Así que se agrega el archivo .pk3 al sistema de archivos 34 irr::io::IFileSystem. Después de cargado, se puede leer cualquier archivo contenido dentro del .pk3 como si estuviera directamente almacenado en el disco. device->getFileSystem()->addZipFileArchive(".

0 downloads 115 Views 1MB Size

Story Transcript

http://cristiandlr.blogspot.com

Cristian de Léon Ronquillo

DESARROLLO DE VIDEOJUEGOS 3D CON SOFTWARE LIBRE

FACULTAD DE INGENIERÍA DE SISTEMAS, INFORMÁTICA Y CIENCIAS DE LA COMPUTACIÓN

QUETZALTENANGO, NOVIEMBRE 2008

i

Este proyecto de carrera fue elaborado por el estudiante Cristian Rafael de León Ronquillo como requisito establecido por la Facultad de Ingeniería de la Universidad Mesoamericana de Quetzaltenango, previo a obtener el título de Ingeniero de Sistemas, Informática y Ciencias de la Computación. Quetzaltenango, noviembre de 2008.

ii

iii

ÍNDICE Introducción Propuesta de proyecto Objetivo general Objetivos específicos Capítulo I – Álgebra lineal 1.1 Antecedentes 1.2 Introducción 1.3 Vectores 1.4 Transformaciones lineales Capítulo II – Irrlicht 3D Engine 2.1 Introducción 2.2 Características 2.3 Configurando el IDE (vs2005) y un ejemplo básico 2.4 Cargar mapas de Quake 3 y agregar una cámara FPS 2.5 Manejo de eventos y movimiento 2.6 Graphical User Inrface (GUI) 2.7 Detección de colisiones 2.8 Audio 2D y 3D con irrKlang 2.9 irrXML Capítulo III – RakNet 3.1 Introducción 3.2 Características 3.3 Incluir RakNet en un proyecto 3.4 Comunicación básica: envío de cadenas 3.5 Irrlicht y RakNet, envío de estructuras Capítulo IV – Aplicación: irrArena 4.1 Introducción 4.2 Características del juego 4.3 Instalación del juego 4.4 Diseño del juego Conclusiones Glosario Bibliografía Apéndice A – Licencia de Irrlicht Engine Apéndice B – Contribuciones y recursos Apéndice C – Contenido del CD

iv

1 3 5 5 7 9 10 10 17 25 26 26 27 33 37 42 47 53 59 61 63 63 64 65 71 81 83 83 84 91 95 97 101 103 105 107

v

INTRODUCCIÓN Los videojuegos representan en la actualidad una de las industrias más grandes del mundo y su popularidad ha venido creciendo de forma paralela a los avances informáticos y tecnológicos con los que se cuentan hoy en día. El presente proyecto de investigación consta de dos partes: Una introducción teórica a los videojuegos 3D y el desarrollo de un videojuego de primera persona. Se inicia desde las bases matemáticas necesarias para trabajar con espacios tridimensionales hasta el uso del Engine de juegos Irrlicht 3D, la API de sonido 3D IrrKlang y la librería de funciones de red RakNet. Con este proyecto de investigación se pretende aportar conocimiento a los estudiantes de ingeniería de sistemas o desarrolladores de software interesados en introducirse al mundo de la programación de videojuegos 3D con software libre. La metodología utilizada para llevar a cabo el presente proyecto es la investigación. Antes de elaborarlo se consultaron diversas fuentes de información relacionadas al tema; se evaluaron características de diversos Engines de videojuegos, tales como Ogre 3D, Blitz3D e Irrlicht 3D; finalmente se tomó la decisión de utilizar Irrlicht 3D por las características que incorpora, la buena estructuración de la API y la amplia documentación con la que cuenta. El presente documento consta de cuatro capítulos, los primeros tres corresponden a la parte teórica del proyecto y cubren los temas de espacios tridimensionales y àlgebra lineal, introducción al Engine Irrlicht 3D, utilización de la API de audio 2D y 3D Irrklang, acceso a archivos de configuración XML por medio de la librería irrXML y utilización de elementos de red por medio de RakNet. La parte demostrativa se detalla en el capítulo cuatro, corresponde al desarrollo de un videojuego de primera persona en red: “irrArena” se abarca no sólo el diseño del mismo, sino también su implementación, utilizando toda la teoría de la primera parte de la investigación. Se espera que este proyecto sea de utilidad a los lectores y que sirva como material de apoyo para futuras investigaciones o proyectos de videojuegos.

1

2

PROPUESTA DE PROYECTO El presente proyecto presenta una investigación sobre algunas tecnologías libres existentes que permiten desarrollar videojuegos 3D, además incluye temas generales acerca de los fundamentos matemáticos de los espacios tridimensionales (Álgebra linal), Irrlicht3D Engine, Manejo de Red con Raknet, acceso a archivos XML por medio de IrrXML, Audio 2D y 3D con IrrKlang y del desarrollo del videojuego de primera persona “irrArena”, aplicando el software antes mencionado. Se compone de cuatro capítulos que explican de forma general y secuencial los fundamentos utilizados para crear un videojuego 3D. Se asume que el lector tiene conocimientos de álgebra y programación, específicamente del lenguaje C++. Los primeros tres capítulos aportan la base teórica y el cuarto capítulo describe el juego irrArena que se incluye en el CD que acompaña a este documento. El capítulo I presenta una introducción a las Matemáticas de los videojuegos. Se tratan temas como sistemas lineales, vectores, interpretación geométrica de las operaciones vectoriales, proyecciones y transformaciones lineales. El capítulo II incluye documentación y ejemplos del Engine Irrlicht 3D, cubriendo temas que van desde el manejador de video, el entorno GUI, manejo de escena, carga de mapas de Quake 3, intercepción de eventos del teclado y del ratón (movimiento), colisiones, iluminación de escena, sonido 2D y 3D con irrKlang y carga de archivos XML con irrXML. En el capítulo III se presenta una introducción a la librería de funciones de Red RakNet, la cual ha sido diseñada específicamente para videojuegos. Se incluyen algunos ejemplos de comunicación de red, tales como el envío de cadenas y una pequeña aplicación que ilustra cómo envíar estructuras de datos genéricos y cómo combinar esta librería con Irrlicht. El capítulo IV es la parte demostrativa del texto. Se desarrolla el videojuego “irrArena”, de primera persona, multijugador, para ello se utiliza Irrlicht3D y Raknet, presentando la documentación correspondiente y el diseño del mismo.

3

4

OBJETIVOS General Presentar una investigación que sirva como un aporte de conocimiento y documentación a los estudiantes de ingeniería y desarrolladores de software interesados en incursionar en mundo de los videojuegos 3D, particularmente con software libre, planteando el presente proyecto de forma demostrativa y práctica.

Específicos 1. Investigar las características y el funcionamiento básico de un Engine de gráficos 3D de tiempo real y aplicar este conocimiento para desarrollar un juego de video.

2. Presentar una introducción a la librería de funciones de red RakNet para programar juegos y aplicaciones de red en general.

3. Diseñar y desarrollar el juego de acción “irrArena” para aplicar y exponer el conocimiento adquirido durante la elaboración de este proyecto.

5

6

CAPÍTULO I

ÁLGEBRA LINEAL

El álgebra lineal es la rama de las matemáticas que estudia conceptos, tales como vectores, matrices, sistemas de ecuaciones lineales y - en un enfoque más formal - espacios vectoriales y transformaciones lineales. En ésta figura se muestra un cubo al que se aplicaron diversas diversas transformaciones de rotación.

7

8

1.1 Antecedentes Un videojuego es un programa informático, creado para el entretenimiento, basado en la interacción entre una o varias personas y un aparato electrónico (ya sea un ordenador, un sistema arcade, una videoconsola o un dispositivo handheld), el cual lo ejecuta. En muchos casos, los videojuegos recrean entornos y situaciones virtuales en los cuales el jugador puede controlar a uno o varios personajes (o cualquier otro elemento), para conseguir ciertos objetivos por medio de reglas determinadas. La popularidad de los videojuegos ha venido aumentando con gran rapidez en las últimas décadas, tanto que su industria representó un valor de 41.9 billones de dólares mundialmente durante el año 2007. Se estima que para 2012 generará unos 68.3 billones de Dólares1, superando así a la industria cinematográfica. En el año de 1961 fue desarrollado Spacewar! – considerado el primer videojuego para computadora – y a partir de esa fecha, la tecnología y la informática han evolucionado para ofrecer nuevas alternativas que mejoran la experiencia del jugador, mostrando entornos más reales. La introducción de Mortal Kombat – un producto que utilizaba Sprites – presentó un avance considerable en la calidad gráfica de los videojuegos. Al operar en dos dimensiones, éste permite manejar un mayor detalle en las animaciones, así como añadir efectos visuales que simulan profundidad, debido a que están basados en mapas de bits dibujados en la pantalla del ordenador. Más adelante, con el videojuego Quake, se introdujeron modelos tridimensionales para los personajes en lugar de utilizar Sprites. En este caso la animación es realizada por medio de bones (huesos virtuales). En la década de 1990 se introdujo el término “Game Engine”, el cual hace referencia a una plataforma que permite manejar los aspectos que son comunes a todos los videojuegos (código reutilizable). Éstos pueden ser: código reutilizable, carga de objetos, visualización y animación de modelos, detección de colisiones entre objetos, física, dispositivos de control como: el teclado, mouse, joystick, etc., interfaces gráficas de usuario e incluso parte de la inteligencia artificial del juego.

1

Study: Video Games on Winning Streak, PC Magazine. http://www.pcmag.com/article2/0,1759,2320607,00.asp

9

1.2 Introducción La Física es una disciplina muy extensa, abarca desde el movimiento simple de un cuerpo, el comportamiento de las ondas mecánicas como el sonido, el comportamiento de la luz y demás ondas electromagnéticas, hasta temas más avanzados como Relatividad y Física Cuántica. Algunos temas de Física son útiles en el desarrollo de videojuegos, por ejemplo, se podría utilizar la óptica para simular la forma en la que la luz viaja e incide sobre los objetos para generar gráficas de alta calidad (Ray-tracing), pero cuando se habla de la Física de los videojuegos se refiere a la mecánica clásica, que consiste en describir el movimiento de sistemas de partículas físicas, de sistemas macroscópicos y a velocidades pequeñas comparadas con la velocidad de la luz. En un videojuego, la mecánica clásica se utiliza para dar a los objetos la sensación de ser sólidos, tener masa, inercia, rebote y cierto grado de capacidad para flotar. Toda la parte física es controlada por el Engine o motor de gráficos 3D, por tanto en este capítulo se hará referencia a conceptos fundamentales del Álgebra lineal, como una introducción a los sistemas lineales, vectores y tranformaciones en el espacio.

1.3 Vectores 1.3.1 SISTEMA LINEAL Muchos problemas pueden modelarse y resolverse por medio de un sistema lineal. Éste consiste en un conjunto de m ecuaciones con n variables (o incógnitas), es decir, un conjunto de m ecuaciones lineales. Las leyes de la mecánica se modelan mediante sistemas lineales. Particularmente interesan los sistemas de tres variables (véase la figura 1.1), donde se puede representar, por ejemplo, la posición en el espacio relativa a un origen de una partícula, mediante las distancias x, y y z trazadas ortogonalmente (a 90o) entre sí.

10

Figura 1.1. Imagen ilustrativa de la posición de una partícula en el espacio respecto al origen. Las flechas que están sobre los ejes representan variables de posición de un sistema lineal de 3 variables (x, y y z) que combinadas denotan la posición de dicha partícula.

1.3.2 VECTORES Algunas medidas físicas como la temperatura, el área, la masa, el volumen y la energía pueden ser representadas simplemente como una magnitud, un número real conocido en álgebra escalar. Sin embargo, cuando se necesita indicar la dirección (además de la magnitud), se utiliza un vector, el cual se define como una magnitud física con dirección y sentido. Por medio de vectores se puede modelar una fuerza, un desplazamiento, una velocidad, una aceleración, etc. Los vectores son objetos algebraicos y geométricos. Este tipo de dualidad permite estudiar la geometría con métodos algebraicos. Un vector es una matriz columna de n x 1 o n-vector, por ejemplo:

1  u =  ,  2

 2 v =  4  ,  6 

 −0.5  −2   w=  3     −1 

Los anteriores son vectores 2, 3 y 4 (en R 2 , R 3 y R 4 ) respectivamente. Los elementos de un vector se llaman componentes. 1.3.3 MÓDULO DE UN VECTOR El módulo de un vector es su magnitud y se define de la siguiente manera:

 x1  x  2 2 2 v = x1 + x2 + ... + xn , donde v =  2  .  ...     xn  11

En la figura 1.2 se aprecia el significado geométrico de la magnitud de un vector.

Figura 1.2. Esta imagen muestra la interpretación geométrica del modulo de un vector, esto es su magnitud desde el origen, denotada por v .

1.3.4 DIRECCIÓN DE UN VECTOR En R 2 la dirección de un vector (véase la figura 1.3) está representada por el ángulo entre el eje x positivo y el vector mismo, mientras que en R 3 la dirección se representa mediante un vector unitario u llamado dirección de v.

Figura 1.3. El ángulo θ entre el vector v y el eje x 2

representa la dirección del vector v en R .

El vector unitario u se define de la siguiente manera:

uv =

v v

Éste tiene la propiedad de tener magnitud 1 y la misma dirección de v. Existen tres 3 vectores unitarios que se usan como base para generar todo el espacio R , estos son:

i, j y k y se definen de la siguiente manera:

12

1  0 0     i =  0  , j = 1  y k = 0  0   0  1  De manera que se puede escribir cualquier otro vector en R 3 como una combinación lineal de estos vectores, por ejemplo:

3 v =  −2  = 3i − 2 j;  0  Otra forma de representar la dirección de un vector en R 3 es mediante sus ángulos directores, tal como se aprecia en la figura 1.4.

Figura 1.4. Los ángulos directores son los que van desde un eje hasta el vector v y definen la dirección del mismo.

1.3.5 OPERACIONES VECTORIALES Y SU INTERPRETACIÓN GEOMÉTRICA 1.3.5.a Suma vectorial

La suma de vectores consiste en sumar las componentes de los mismos, es decir, la suma de las componentes x será la componente x del vector resultante, y de esta misma manera las componentes y, z, …, se suman para formar las componentes y, z, … del vector resultante, por ejemplo: Sea u = 3i – j, y v = –i +2j – 2k. Hallar u + v u + v = (3 –1)i + (–1 +2)j + (–2)k = 2i + j – 2k; Gráficamente esto es equivalente a colocar, sin importar el orden, (propiedad conmutativa de la suma) el origen de cada vector al final de otro vector, el vector resultante es aquél que se dirige desde el origen del primer vector hasta el final del último, como se aprecia en la figura 1.5.

13

Figura 1.5.. (a) Tres vectores en el espacio (u, v y w). (b) El vector R representa la suma de los vectores u, v y w.

1.3.5.b Resta vectorial Para restar vectores algebraicamente se cambian los signos del vector a restar (sustraendo) y luego se suman ambos vectores. Sea u = 2i – 3j + k, y v = 2i +j – k. Hallar u – v

u – v = 2i – 3j + k – (2i +j – k) = –4j; Al restar dos vectores se obtiene un tercer vector dirigido desde el extremo del vector sustraendo hasta el extremo del vector minuendo, como se ve en la figura 1.6.

Figura 1.6 6. Interpretación geométrica de la resta de vectores. El vector v – u se dirige desde el extremo de u hasta el extremo de v.

1.3.5.c Producto de un escalar por un vector Para obtener el producto de un vector por un escalar, simplemente se multiplica el escalar por cada uno de los componentes del vector. Esto provoca un aumento (o disminución) proporcional al escalar en la magnitud del vector. Véase V la figura 1.7.

14

Figura 1.7. 1. (a) Un vector u en el espacio. (b) El vector u multiplicado por el escalar c.

Cuando ambos factores en el producto son vectores, se pueden realizar dos tipos de producto: uno que genere un escalar, escalar llamado producto escalar (también ( llamado producto interior o producto punto) o bien un producto que genere un nuevo vector, llamado producto vectorial (también llamado producto exterior o producto cruz). 1.3.5.d Producto escalar

 a1   b1  a  b  2 2  Sean a = y b =   dos vectores, entonces ntonces el producto escalar de a y b ,  ...   ...       an  bn  denotado por a ⋅ b está dado por: a ⋅ b = a1b1 + a2b2 + ... + an bn Ambos vectores deben tener el mismo número de elementos. Geométricamente se puede interpretar el producto escalar de la siguiente manera: El producto (escalar) de dos vectores no nulos es igual al módulo de uno de ellos por la proyección del otro sobre él. véase la figura f 1.8.) de un vector u sobre otro vector v (diferentes La proyección (véase de cero) se define de la siguiente manera: manera

proy v u =

15

u ⋅v v

2

v

Figura 1.8. (a) proy v u cuando el ángulo θ entre ambos vectores (u y v) es menor que

π 2

. (b) proy v u cuando el ángulo θ entre ambos

vectores (u y v) es mayor que

π 2

.

1.3.5.e Producto vectorial

 a1   b1      Sean u y v dos vectores tales que, u = a2 y v = b2 el producto cruz entre      a3  b3  j i  ambos está definido por la determinante de la matriz a1 a2   b1 b2 i

j

u × v = a1 b1

a2 b2

k a3  , de forma que: b3 

k

a3 = (a2b3 − a3b2 )i − (a1b3 − a3b1 ) j + (a1b2 − a2b1 )k; b3

Donde i , j y k son los vectores unitarios (bases de R3 ). El producto cruz está definido únicamente para R 3 y gráficamente u × v es un vector ortogonal, tanto a u como a v (vector normal n ). Hay dos vectores que son ortogonales, tanto a u como a v : u × v y v × u ; la dirección del vector u × v se determina por medio de la regla de la mano derecha2. Si se coloca la mano derecha de manera que el índice apunte en la dirección de u y el dedo medio en la dirección de v , entonces el pulgar apuntará en la dirección de u × v . Véase la figura 1.9.

2

Algebra lineal. Stanley I. Grossman. El producto cruz de dos vectores, pág. 263. Editorial McGraw-Hill, 1996.

16

Figura 1.9. La dirección de

u × v se puede

determinar usando regla de la mano derecha.

Algunas propiedades importantes del producto cruz se enuncian a continuación: Sean • • • • • •

u , v y w tres vectores en R3 y sea a un escalar cualquiera, entonces: u × 0 = 0 × u = 0. u × v = −(v × u ). (Propiedad anticonmutativa) ( au ) × v = a (u × v). u × (v + w) = (u × v) + (u × w). (Propiedad distributiva) (u × v) ⋅ w = u ⋅ (v × w). (Triple producto escalar) u ⋅ (u × v) = v ⋅ (u × v) = 0. ( u × v es ortogonal a u y a v )

1.4 Transformaciones lineales En los gráficos computacionales, las transformaciones lineales sirven para dar la sensación de movimiento en tiempo real a un determinado objeto en el espacio, se puede por ejemplo, cambiar la posición del objeto (traslación), rotar el objeto en torno a un eje, aumentar o disminuir su tamaño (escalar el objeto), sesgar el objeto o bien realizar cualquier combinación de lo anterior, simultáneamente.

Figura 1.10. Transformación de una imagen. Una transformación3 T (también llamada mapeo o función) de un conjunto A a un conjunto B , representada por T : A → B , es una regla que asocia a cada elemento de

A un elemento b , único de B , llamado imagen de a bajo T . Se escribe T (a) = b y se dice que a se mapea a T ( a ) . A se llama dominio de T . B es codominio de T . El

3

Definición de transformación: Algebra lineal con aplicaciones. Transformaciones lineales, pág. 307. George Nakos y David Joyner. Editorial Thomson, 1999.

17

subconjunto de B formado por todas las imágenes de los elementos de A se llama rango o contradominio de T y se representa por R (T ) . Una transformación se llama lineal cuando cumple con las siguientes características: Sean • •

u y v dos vectores y a un escalar, entonces T (u + v) = T (u ) + T (v). T (cu ) = cT (u ).

Se puede transformar cualquier punto (un vector en R 3 ) en otro, usando una matriz de transformación (de 4×4 para R 3 , esto es necesario por la transformación de traslación que verá más adelante). Se usan matrices porque permiten modificar cada componente en un vector respecto de los tres ejes del espacio. En el siguiente ejemplo, una matriz reinterpreta el punto (x, y, z), produciendo el nuevo punto (x', y', z'): Simplemente se multiplica el vector (punto a transformar)

v = [x

y

z 1]

por la matriz de transformación

 M 11 M M =  21  M 31   M 41

M 12

M 13

M 22 M 32 M 42

M 23 M 33 M 43

M 14  M 24  M 34   M 44 

así:

[x '

y ' z ' 1] = [ x

y

 M 11 M z 1]  21  M 31   M 41

M 12 M 22 M 32

M 13 M 23 M 33

M 42

M 43

M 14  M 24  M 34   M 44 

De forma que:

x ' = xM 11 + yM 21 + zM 31 + 1M 41 , y ' = xM 12 + yM 22 + zM 32 + 1M 42 , x ' = xM 13 + yM 23 + zM 33 + 1M 43 . Se pueden combinar las matrices de transformación en una matriz simple para calcular varias transformaciones en una sola operación (esta operación es conocida como

18

concatenación). Esto se consigue multiplicando las matrices individuales transformación (Véase la definición de Skeletal Animation en el glosario).

de

1.3.1 EJEMPLOS DE TRANSFORMACIONES LINEALES COMUNES A continuación se examinará brevemente algunas matrices de transformación. 1.3.1.a Escalado Para cambiar el tamaño de un objeto en R 3 se aplica la siguiente transformación (tal como se indicó anteriormente) a cada uno de los vértices que lo conforman:

 sx 0  0  0

0 sy 0 0

0 0 sz 0

0 0  0  1

Donde s x , s y y s z son números reales que determinan las escalas de los ejes x, y y z respectivamente. Por ejemplo, si se desea duplicar el tamaño de un triángulo cuyos vértices son

[0

0 0] , [1 1 0] y [ 2 0 0] basta con multiplicar cada uno de estos vectores por la

2 0 matriz de transformación  0  0

0 2 0 0

0 0 2 0

0 0 para obtener los vértices del triángulo 0  1

transformado (véase la figura 1.11). 2 0 [0 0 0 1]  0  0 2 0 [1 1 0 1]  0  0 2 0 [ 2 0 0 1]  0  0

0 0 0 2 0 0  = [0 ⋅ 2 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ 0 0 ⋅ 0 + 0 ⋅ 2 + 0 ⋅ 0 + 1 ⋅ 0 0 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 2 + 1 ⋅ 0 0 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅1] = [ 0 0 0 1] 0 2 0  0 0 1 0 0 0 2 0 0  = [1 ⋅ 2 + 1 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ 0 1 ⋅ 0 + 1 ⋅ 2 + 0 ⋅ 0 + 1 ⋅ 0 1 ⋅ 0 + 1 ⋅ 0 + 0 ⋅ 2 + 1 ⋅ 0 1 ⋅ 0 + 1 ⋅ 0 + 0 ⋅ 0 + 1 ⋅1] = [ 2 2 0 1] 0 2 0  0 0 1 0 0 0 2 0 0  = [2 ⋅ 2 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅ 0 2 ⋅ 0 + 0 ⋅ 2 + 0 ⋅ 0 + 1 ⋅ 0 2 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 2 + 1 ⋅ 0 2 ⋅ 0 + 0 ⋅ 0 + 0 ⋅ 0 + 1 ⋅1] = [ 4 0 0 1] 0 2 0  0 0 1

19

Figura 1.11. 1.1 (a) Un triángulo definido por sus vértices. (b) El resultado de aplicar una transformación de tamaño al triángulo del inciso (a) por un factor de 2.

1.3.1.b Rotación Se puede rotar un punto alrededor de un eje, eje para ello se cuenta con tres matrices de rotación, una para cada eje, pueden combinarse (como ya se mencionó) multiplicando las matrices entre sí. Nota: Las matrices de rotación definidas aquí corresponden a un sistema de mano izquierda,, como se aprecia en la ffigura 1.12.

Figura 1.12. 1.1 En un sistema de mano izquierda, la dirección positiva del eje z es la que se aleja del observador. En un sistema de mano derecha ocurre lo contrario, la dirección positiva del eje z es la que se acerca al observador.

La siguiente transformación rota el punto (x, y, z)) sobre el eje x, produciendo un nuevo punto (x', y', z'):

[x'

y ' z ' 1] = [ x

y

0 1 0 cos θ z 1]  0 − sin θ  0 0

20

0 sin θ cos θ 0

0 0  0  1

La siguiente transformación rota el punto sobre el eje y:

[x '

y ' z ' 1] = [ x

cos θ  0 z 1]   sin θ   0

y

0 − sin θ 1 0 0 cos θ 0 0

0 0  0  1

La siguiente transformación rota el punto sobre el eje z:

[x '

y ' z ' 1] = [ x

 cos θ  − sin θ z 1]   0   0

y

sin θ cos θ 0 0

0 0 1 0

0 0  0  1

Donde θ simboliza el ángulo de rotación en radianes. Los ángulos se miden en el sentido horario cuando se mira sobre el eje de rotación hacia el origen. 1.3.1.c Traslación La traslación permite –como su nombre lo indica– trasladar todos los vértices que conforman un objeto en el espacio a una nueva posición. El resultado de esta operación es equivalente a sumar Tx , Ty y Tz a las coordenadas obteniendo el vector  x + Tx y + Ty z + Tz punto (x, y, z) a un nuevo punto (x', y', z').

[x '

y ' z ' 1] = [ x

x , y y z , respectivamente,

1 . La siguiente transformación traslada el

y

1 0 z 1]  0  Tx

0

0

1 0 Ty

0 1 Tz

0 0  0  1 

1.3.1.d Sesgado Esta transformación (también conocida como corte o deslizamiento) permite deslizar un punto a través de un eje determinado. La figura 1.10 es un ejemplo de transformación de corte relativo al eje x en 2D. Las figuras 1.13 y 1.14 muestran un cubo en el espacio que ha sido transformado, primero respecto al eje x (figura 1.13) y luego respecto al eje y (figura 1.14).

21

La siguiente transformación desliza el punto (x, ( y, z)) por un factor c en el eje x (plano xy),, produciendo un nuevo punto (x', ( y', z').

[x'

y ' z ' 1] = [ x

y

1 c z 1]  0  0

0 1 0 0

0 0 1 0

0 0  = [ x + cy 0  1

y

z 1]

Figura 1.13. 1.1 La figura muestra el efecto que produce un deslizamiento en el eje x (utilizando el plano xy).

La siguiente transformación desliza el un punto sobre el eje y (plano xy):

[x'

y ' z ' 1] = [ x

y

1 0 z 1]  0  0

c 1 0 0

0 0 1 0

0 0  = [ x cx + y 0  1

z 1]

Figura 1.15. La figura muestra el efecto que produce un deslizamiento en el eje y (utilizando el plano xy).

En general, se puede realizar un deslizamiento en cualquier eje utilizando un plano determinado y las transformaciones se pueden expresar como una función de la siguiente manera: Deslizamiento en x, plano xy: xy S x ( x, y , z ) = ( x + cy, y, z ). Deslizamiento en y, plano xy: xy S y ( x, y, z ) = ( x, y + cx, z ). Deslizamiento en x, plano xz: xz S x ( x, y , z ) = ( x + cz , y, z ). Deslizamiento en z, plano xz: xz S z ( x, y , z ) = ( x, y , z + cx ). 22

Deslizamiento en y, plano yz: S y ( x, y, z) = ( x, y + cz, z ). Deslizamiento en z, plano yz: S z ( x, y , z ) = ( x, y, z + cy ). El factor c puede ser tanto positivo como negativo, esto se refleja en la dirección del eje sobre el que se realiza el deslizamiento.

23

24

Capítulo II

Irrlicht3D Engine

Indoor Rendering es una característica integrada en el Engine que permite utilizar mapas de luz y superficies curvas; adicionalmente existe un detector de colisiones y un sistema de respuesta.

25

2.1 Introducción Irrlicht3D es un motor de gráficos 3D de alto rendimiento y de código abierto escrito en C++ y utilizable tanto en C++ como en Lenguajes .NET. Es completamente independiente de la plataforma, utilizando Direct X, Open GL o su propio software de renderizado y tiene todas las características que pueden ser encontradas en otras plataformas 3D comerciales. 2.2 Características Irrlicht es un Engine 3D de tiempo real, multi-plataforma y de alto desempeño escrito en lenguaje C++. Es una poderosa API4 de alto nivel para la creación de completas aplicaciones 3D y 2D tal como juegos o aplicaciones para visualización científica. Viene con una excelente documentación e integra todas las características actualmente utilizadas para la representación visual como sombras dinámicas, sistemas de partículas, animación de personajes, tecnología interior y exterior (indoor and outdoor technology) y detección de colisiones. Cabe mencionar que este software está actualmente en desarrollo. La información acerca de los avances en el desarrollo del proyecto está disponible en la siguiente página web: http://irrlicht.sourceforge.net/development.html. Sus principales características5 son:

• Renderizado 3D en tiempo real de alto desempeño utilizando Direct3D y OpenGL. • Independiente de la plataforma. Compatible con Windows 95, 98, NT, 2000, XP, Vista, GNU/Linux, y MacOS.

• Incorpora una enorme y extensible librería de materiales con soporte para Pixel Shaders y Vertex Shaders.

• Manejo de escenas altamente personalizable para interiores y exteriores. • Sistema de animación de modelos con esqueletos (Skeletal Animation) y animación de vértices (Morph target animation).

• Efectos de partículas, billboards, mapas de luz, mapeo de entorno, stencil buffer shadows y muchos otros efectos especiales.

• Bindings para .NET, lo que hace que el Engine esté disponible para cualquier lenguaje de la plataforma .NET tal como C#, VisualBasic y Delphi.NET.

• Incluye dos rápidos renderizadores por software independientes, tanto de la plataforma como del driver de video que tienen diferentes propiedades: Corrección de texturas mapeadas en perspectiva, filtrado bilineal, corrección sub-píxel, z-buffer, Gouraud shading, alpha-blending y transparencias, dibujo 2D rápido y más. 4 5

API, del inglés: Aplication Programming Interface o Interfaz de programación de aplicaciones. Fuente: Irrlicht3D Features, http://irrlicht.sourceforge.net/features.html

26

• Sistema de interfaz gráfica de usuario (GUI) personalizable y fácil de usar. Incluye botones de comando, listas, cajas de texto, etc.

• Funciones de dibujo en 2D: alpha blending, blitting, dibujo de fuentes y mezcla de gráficas 2D y 3D.

• Una API bien documentada y correctamente estructurada. • Escrito completamente en lenguaje C++ y totalmente orientado a objetos. • Funciones para lectura directa de los formatos más comunes de objetos 3D, entre ellos: 3D Studio meshes (.3ds), B3D files (.b3d), Alias Wavefront Maya (.obj), Cartography shop 4 (.csm), COLLADA (.xml, .dae), DeleD (.dmf), FSRad oct (.oct), Irrlicht scenes (.irr), Irrlicht static meshes (.irrmesh), Microsoft DirectX (.x) (binary & text), Milkshape (.ms3d), My3DTools 3 (.my3D), OGRE meshes (.mesh), Pulsar LMTools (.lmts), Quake 3 levels (.bsp), Quake 2 models (.md2) y STL 3D files (.stl).

• Funciones de lectura directa de formatos de textura: Adobe Photoshop (.psd), JPEG File Interchange Format (.jpg), Portable Network Graphics (.png), Truevision Targa (.tga), Windows Bitmap (.bmp) y Zsoft Paintbrush (.pcx).

• Rápido sistema de detección de colisiones. • Librerías de contenedores de plantillas de funciones matemáticas 3D rápidas y optimizadas.

• • • •

Lectura directa de archivos comprimidos (.zip). Parser (Analizador sintáctico) de archivos XML integrado (irrXML). Soporte para Unicode. Compatible con Microsoft VisualStudio6.0™, VisualStudio.NET 7.0-8.0™, Metrowerks Codewarrior y Bloodshed Dev-C++ con g++3.2-4.0.

• El Engine es de código abierto y completamente libre. Puede ser depurado, corregido e incluso modificado sin la obligación de hacer públicos dichos cambios: El Engine está bajo los términos de la licencia zlib6.

2.3 Configurando el IDE (Visual Studio 2005) y un ejemplo básico Como se ha descrito en la sección 1.2, Irrlicht puede ser incorporado en una amplia variedad de Entornos de Desarrollo Integrados (IDEs), siendo los pasos para la integración similares en los múltiples entornos de desarrollo. Aquí se describe cómo configurarlo con uno de los entornos de desarrollo más utilizados por los desarrolladores de software7; éste es el caso de Microsoft Visual Studio (actualmente puede obtenerse gratuitamente la versión Express en http://www.microsoft.com/Express/). El SDK de Irrlicht puede obtenerse en http://irrlicht.sourceforge.net/downloads.html, la versión actual es la 1.4.2 e incluye la documentación, tutoriales, exportadores y varios

6 7

Véase el Apéndice A. Licencia del Engine Irrlicht3D. Texto Original - Tutorial 1: HelloWorld - http://irrlicht.sourceforge.net/tut001.html

27

programas de demostración, incluyendo el código fuente y archivos necesarios para correr todos los ejemplos que se detallan a continuación. 2.3.1 CONFIGURANDO EL IDE Para utilizar el Engine se debe incluir el archivo de cabecera irrlicht.h. Éste archivo se encuentra en el directorio include del SDK. Para permitir al compilador acceder a dicho archivo, se debe configurar la ruta al mismo. En Microsoft Visual Studio se debe realizar el siguiente procedimiento: 1. Seleccionar Opciones (Options) del menú de Herramientas (Tools) 2. Seleccionar la sección de Proyectos y Soluciones (Projects And Solutions) y buscar la opción de Directorios de VC++ (VC++ Directories) como se vé en la Figura 2.2.

Figura 2.2. Cuadro de opciones del menú Herramientas Visual Studio 2005. 3. Seleccionar Include Files del combo que dice Mostrar directorios para (Show directories for) como se muestra en la Figura 2.3.

Figura 2.3. Opción “Show directories for” del cuadro de opciones de Visual Studio 2005. . Dicho direcotrio se encuentra en la 4. Agregar el directorio include con el botón carpeta de instalación del SDK. 5. También es necesario agregar la librería irrlicht.lib, para ello se debe seleccionar la opción Archivos de Librería (Library files) del combo Mostrar directorios para (Show directories for) y agregar el directorio lib/VisualStudio incluído en el SDK.

28

2.3.2 HOLA MUNDO ! El siguiente programa ejemplifica los aspectos básicos del Driver de video, el entorno GUI, y el manejo de escena. Lo primero que se debe hacer es incluir el archivo de cabecera irrlicht.h #include

En el Engine Irrlicht todo se encuentra dentro del nombre de espacio irr. Para utilizar cualquiera de las clases del Engine se debe escribir irr:: antes del nombre de la clase. Por ejemplo, para acceder a la clase IrrlichtDevice, se debe escribir irr::IrrlichtDevice. Para evitar escribir irr:: antes de cada clase, se indica al compilador que se utilizará ese nombre de espacio de ahora en adelante y no habrá que escribir irr:: nuevamente. using namespace irr;

En el Engine Irrlicht hay cinco sub-espacios. Se puede encontrar información detallada de ellos en la documentación: http://irrlicht.sourceforge.net/docu/namespaces.html De la misma forma en la que se declaró el nombre de espacio irr, se declara la utilización de estos cinco nombres de espacio para mantener este ejemplo fácil de entender. using namespace core; using namespace scene; using namespace video; using namespace io; using namespace gui;

Para poder utilizar el archivo Irrlicht.DLL, se debe vincular la aplicación con Irrlicht.lib. Ésta vinculación se puede establecer en las configuraciones del proyecto, pero para facilitar las cosas, se utilizará la instrucción pragma comment de Visual Studio. En plataformas Windows, se debe ocultar la ventana de consola, la cual salta cuando se inicia el programa en main(). Esto se hace con la segunda instrucción pragma. También se puede utilizar el método WinMain(), pero de esta manera la aplicación ya no es independiente de la plataforma (sistema operativo). #ifdef _IRR_WINDOWS_ #pragma comment(lib, "Irrlicht.lib") #pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup") #endif

29

Éste es el método main(), el cual es utilizado por cualquier plataforma. int main() {

La función más importante del Engine es createDevice(). El Dispositivo Irrlicht es creado al llamarla. Éste es el objeto raíz para hacer cualquier cosa con el Engine. createDevice() tiene siete parámetros: •

deviceType: tipo de dispositivo. Puede ser un dispositivo nulo, D3D8, D3D9, OpenGL o bien alguno de los dos renderizadores de software incluídos en el Engine. En este ejemplo se usa el EDT_SOFTWARE, pero esto puede ser sustituido por EDT_BURNINGSVIDEO, EDT_NULL, EDT_DIRECT3D8, EDT_DIRECT3D9, o EDT_OPENGL, según el tipo de dispositivo que se requiera.



windowSize: define el tamaño de la ventana, o bien, la resolución de la pantalla cuando se está ejecutando en modo de pantalla completa. En este ejemplo se usa 640x480.



bits: define la cantidad de bits de color por píxel, 16 ó 32. El parámetro es generalmente ignorado cuando se ejecuta en modo de ventana.



fullscreen: especifica si se va a ejecutar en pantalla completa o en modo de ventana.



stencilbuffer: especifica si se va a utilizar stencil buffer (para dibujar sombras).



vsync: especifica la activación o desactivación de vsync, esto solo es útil en el modo de pantalla completa.



eventReceiver: es un objeto para interceptar eventos. Por ahora no se utiliza, así que se establecrá su valor a 0.

Siempre se debe verificar el valor de retorno para determinar controladores no soportados, dimensiones no válidas, etc. IrrlichtDevice *device = #ifdef _IRR_OSX_PLATFORM_ createDevice(video::EDT_OPENGL, dimension2d(640, 480), 16, false, false, false, 0); #else createDevice(video::EDT_SOFTWARE, dimension2d(640, 480), 16, false, false, false, 0); #endif if (!device) return 1;

Ahora se establecerá el título de la ventana. Notese que hay una ‘L’ antes de la cadena. El Engine Irrlicht utiliza cadenas de caracteres anchos para desplegar texto. 30

device->setWindowCaption(L"Hello World! - Irrlicht Engine Demo");

Ahora se crea un puntero al driver de video, el manejador de escena y el entorno de interfaz gráfica de usuario, de esta forma no se tendrá que escribir siempre: device>getVideoDriver(), device->getSceneManager(), o device->getGUIEnvironment(). IVideoDriver* driver = device->getVideoDriver(); ISceneManager* smgr = device->getSceneManager(); IGUIEnvironment* guienv = device->getGUIEnvironment();

Se añade una etiqueta a la ventana con el texto Hello World, usando para ello el entorno GUI. El cuadro de texto estático es posicionado en (10, 10) como la esquina superior izquierda y (260, 22) como la esquina inferior derecha. guienv->addStaticText( L"Hello World! This is the Irrlicht Software renderer!", //cadena a desplegar rect(10,10,260,22), //dimensiones de la caja de texto true //borde de la caja de texto );

Para mostrar algo más interesante, se cargará y desplegará en la escena un modelo MD2. Únicamente se debe cargar la malla desde el manejador de escena con getMesh() y agreagar un nodo de escena para desplegar la malla con addAnimatedMeshSceneNode(). Es recomendable verificar el valor de retorno de getMesh() para asegurar que no haya ocurrido ningún problema al abrir el archivo. En lugar de escribir el nombre de archivo sydney.md2, es posible cargar un archivo de objeto de Maya (.obj), un mapa completo de Quake 3 (.bsp) o cualquier otro formato de archivo soportado por el Engine (véase el listado de características para más información al respecto, Sección 2.2). El modelo sydney.md2 fue diseñado por Brian Collins. IAnimatedMesh* mesh = smgr->getMesh("../../media/sydney.md2"); if (!mesh) return 1; IAnimatedMeshSceneNode* node = smgr->addAnimatedMeshSceneNode( mesh );

Se debe cargar un material a la malla para que se vea mucho mejor. También se desactivará la iluminación porque por ahora no se ha agregado ninguna luz dinámica, de otra forma el modelo se vería completamente negro. Luego se establece el frame loop o ciclo de animación al estado predefinido STAND y por último se aplica una textura a la malla. Sin ésta última la malla sería dibujada, utilizando únicamente un color. if (node) { node->setMaterialFlag(EMF_LIGHTING, false); //desactivar iluminación node->setMD2Animation(scene::EMAT_STAND); //frame loop node->setMaterialTexture( //Aplicar la textura a la malla node 0, //capa de la textura driver->getTexture("../../media/sydney.bmp") //archivo de la textura

31

); }

Para hacer la malla visible en la pantalla se agrega una cámara en el espacio 3D, en la posición (0, 30, -40). La cámara verá desde allí hasta la posición (0, 5, 0), que es aproximadamente el lugar donde se cargó el modelo MD2. smgr->addCameraSceneNode(0, vector3df(0,30,-40), vector3df(0,5,0));

El siguiente paso es configurar la escena y dibujar todo: Se ejecuta el dispositivo en un ciclo while(), hasta que el dispositivo no desee continuar. Esto podría ocurrir cuando el usuario cierra la ventana o presiona Alt + F4 (o cualquier otro comando que cierre la ventana). while(device->run()) {

Cualquier cosa puede ser dibujada entre una llamada a beginScene() y una a endScene(). La llamada a beginScene() limpia la pantalla con un color determinado y el buffer de profundidad (depth buffer) si así se requiere. Luego se deja al manejador de escena y al entorno GUI dibujar su contenido. Con una llamada a endScene() todo es desplegado en la pantalla. driver->beginScene(true, true, SColor(255,100,101,140)); smgr->drawAll(); guienv->drawAll(); driver->endScene(); }

Después que se ha terminado con el ciclo de renderizado, se debe eliminar el dispositivo Irrlicht creado previamente con createDevice(). En el Engine Irrlicht se deben eliminar todos los objetos que se hayan creado con algún método o función que inicie con create. El objeto será eliminado simplemente con una llamada al método ->drop(). Para más información véase el artículo irr::IReferenceCounted::drop() en la documentación del Enigne. device->drop(); return 0; }

Solo hace falta compilar y correr la aplicación. A continuación una muestra de la pantalla del programa en ejecución en la figura 2.4.

32

Figura 2.4. Una muestra de la aplicación de ejemplo ‘Hola Mundo’ ejecutándose. 2.4 Cargar mapas de Quake 3 y agregar una cámara FPS En esta sección se explica como cargar desde el Engine un mapa del juego Quake 3, crear un nodo de escena para optimizar la velocidad de renderizado y cómo crear y utilizar una cámara controlada por el usuario (First Person Shooter Camera o Cámera FPS) Se inicia como en la sección anterior, incluyendo el archivo de cabecera del Engine y un archivo adicional, iostream, para permitir al usuario seleccionar desde la consola el driver que se va a utilizar. #include #include

Como ya se mencionó en el ejemplo de la sección anterior, en el Engine Irrlicht todo se encuentra dentro del nombre de espacio ‘irr’. Para evitar escribir irr:: antes del nombre de cada clase del Engine se indica al compilador que se utilizará ese nombre de espacio de aquí en adelante. Hay otros cinco nombres de espacio (subepacios) dentro del nombre de espacio irr, estos son: core, scene, video, io y gui. A diferencia del ejemplo anterior, no se declara la utilización de estos nombres de espacio para distinguir las clases contendidas dentro de cada uno de ellos. using namespace irr;

De nuevo, para poder utilizar el archivo Irrlicht.DLL, se debe vincular con el archivo Irrlicht.lib. Se utiliza una instrucción pragma comment lib: #ifdef _MSC_VER #pragma comment(lib, "Irrlicht.lib") #endif

33

De nuevo, se utiliza el método main() para iniciar (en lugar de WinMain()). int main() {

Ahora se crea el dispositivo con createDevice(). La diferencia ahora es que se consultará al usuario qué controlador desea utilizar. El dispositivo de software podría ser muy lento para dibujar un mapa enorme de Quake 3; para propósitos de prueba se hará de ésta una opción del menú. // Elegir manejador de video video::E_DRIVER_TYPE driverType; printf("Por favor elija un manejador de video para la aplicación:\n"\ " (a) Direct3D 9.0c\n"\ " (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\ " (d) Software Renderer\n"\ " (e) Burning's Software Renderer\n"\ " (f) NullDevice\n (otherKey) exit\n\n" ); char i; std::cin >> i; switch(i) { case 'a': driverType = video::EDT_DIRECT3D9;break; case 'b': driverType = video::EDT_DIRECT3D8;break; case 'c': driverType = video::EDT_OPENGL; break; case 'd': driverType = video::EDT_SOFTWARE; break; case 'e': driverType = video::EDT_BURNINGSVIDEO;break; case 'f': driverType = video::EDT_NULL; break; default: return 1; } // Crear el dispositivo o salir si hay una falla IrrlichtDevice *device = createDevice(driverType, core::dimension2d(640, 480)); if (device == 0) return 1; //No se pudo crear el dispositivo.

Se crea un puntero al manejador de video y otro para el manejador de escena, de esta manera no es necesario tener que llamar siempre a irr::IrrlichtDevice::getVideoDriver() y irr::IrrlichtDevice::getSceneManager(). video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager();

Para desplegar el mapa de Quake 3 primero se debe cargar. Los mapas de Quake 3 están empaquetados dentro de archivos .pk3, los cuales no tienen ninguna diferencia con los archivos .zip. Así que se agrega el archivo .pk3 al sistema de archivos 34

irr::io::IFileSystem.

Después de cargado, se puede leer cualquier archivo contenido dentro del .pk3 como si estuviera directamente almacenado en el disco. device->getFileSystem()->addZipFileArchive("../../media/map-20kdm2.pk3");

Ahora se puede cargar la malla llamando a irr::scene::ISceneManager::getMesh(). Dicho método devuelve un puntero irr::scene::IAnimatedMesh. Como Ud. probablemente sabe, los mapas de Quake 3 no son realmente animados, simplemente contienen una enorme cantidad de geometría con algunos materiales adjuntos, de ahí que IAnimatedMesh consiste en un único fotograma, así que se debe obtener el “primer fotograma” de la “animación”, el cual representa el nivel de Quake y se crea un nodo de escena de tipo OctTree, usando para ello irr::scene::ISceneManager::addOctTreeSceneNode(). El OctTree optimiza un poco la escena, tratando de dibujar únicamente la geometría que es visible a cada momento mientras se ejecuta la aplicación. Una alternativa a OctTree podría ser irr::scene::IMeshSceneNode, el cual siempre dibuja la geometría completa de la malla, aún cuando no es visible en la pantalla. Se puede probar a utilizar irr::scene::ISceneManager::addMeshSceneNode() en lugar de addOctTreeSceneNode() para comparar las primitivas dibujadas por el manejador de video. Para contar el número de primitivas dibujadas se puede hacer uso del método llamado irr::video::IVideoDriver::getPrimitiveCountDrawn(), contenido dentro de la clase irr::video::IVideoDriver. Note que la optimización que se obtiene con OctTree solo es útil cuando se están dibujando enormes mallas conformadas por una gran cantidad de geometría. scene::IAnimatedMesh* mesh = smgr->getMesh("20kdm2.bsp"); scene::ISceneNode* node = 0; if (mesh) node = smgr->addOctTreeSceneNode(mesh->getMesh(0), //fotograma 0 de la malla 0, //Nodo padre -1, //ID del nodo 128 //Cantidad mínima de polígonos por nodo );

El mapa que se utiliza en este ejemplo no fue modelado alrededor del origen (0,0,0), se debe transformar por medio de los métodos irr::scene::ISceneNode::setPosition(), irr::scene::ISceneNode::setRotation() e irr::scene::ISceneNode::setScale(). if (node) node->setPosition(core::vector3df(-1300,-144,-1249));

Ahora hace falta agregar una cámara que visualice el mapa. Por ahora se necesita una cámara controlada por el usuario. Hay varias cámaras disponibles en el Engine Irrlicht. Por ejemplo la cámara de Maya (MayaCamera) puede ser controlada como una cámara en Maya: rota cuando el botón izquierdo del ratón está presionado, acerca o aleja (Zoom) cuando ambos botones están presionados y traslada con el botón derecho 35

presionado. Para agregar una cámara de Maya se utiliza irr::scene::ISceneManager:: addCameraSceneNodeMaya(). Para este ejemplo se va a utilizar una cámara que se comporta como los juegos de primera persona (FPS game o First Person Shooter game) y para ello se debe utilizar el método irr::scene::ISceneManager::addCameraSceneNodeFPS(). smgr->addCameraSceneNodeFPS();

El puntero del ratón debería ser invisible al usuario, para ello se establece la visibilidad a false por medio del método irr::IrrlichtDevice::ICursorControl. device->getCursorControl()->setVisible(false);

Ahora que se tiene la escena casi lista, solo resta dibujarla. Se desplegará la cantidad de fotogramas por segundo (fps) y la cantidad de primitivas dibujadas en la barra de título de la ventana. La verificación de irr::IrrlichtDevice::isWindowActive() es opcional, pero previene al Engine de seguir al puntero del mouse en la escena cuando se cambia a otra aplicación activa mientras el programa sigue en ejecución. La llamada a irr::IrrlichtDevice::yield() evitará que el ciclo principal del programa utilice todos los ciclos de la CPU cuando la ventana no está activa. int lastFPS = -1; while(device->run()) { if (device->isWindowActive()) { //la ventana está activa driver->beginScene(true, true, video::SColor(255,200,200,200)); smgr->drawAll(); driver->endScene(); int fps = driver->getFPS(); if (lastFPS != fps) { core::stringw str = L"Irrlicht Engine - Quake 3 Map example ["; str += driver->getName(); str += "] fotogramas/segundo:"; str += fps; device->setWindowCaption(str.c_str()); lastFPS = fps; } } else { //la ventana no está activa device->yield(); } }

Por último, se elimina el dispositivo Irrlicht.

36

device->drop(); return 0; }

Eso es todo. Luego de compilar y ejecutar la aplicación se verá como se muestra en la figura 2.5.

Figura 2.5. Unaa pantalla del mapa de Quake 3 Return to Castle corriendo desde el Engine Irrlicht.

2.5 Manejo de eventos ventos y movimiento movimient En esta sta sección se describe cómo mover y animar nodos odos de escena, utilizando para ello Animators (SceneNodeAnimators). También se realizará movimiento manual de nodos utilizando el teclado por medio de un objeto recibidor de eventos. Como siempre, se incluyen los archivos de cabecera, el nombre de espacio irr y un vínculo con el archivo Irrlicht.lib Irrlicht . #include #include using namespace irr; #ifdef _MSC_VER #pragma comment(lib, "Irrlicht.lib") #endif

Para recibir los eventos de teclado, mouse y de interfaz de usuario (por ejemplo cuando se ha presionado un botón de comando) se necesita un objeto que se derive de la clase irr::IEventReceiver. Hay solo un método que se debe sobrescribir: irr::IEventReceiver::OnEvent(). Este método será llamado por el Engine cada vez que un evento ocurra. Lo que realmente se debe saber es si una na tecla está siendo presionada,

37

entonces se puede verificar el estado actual de cada tecla para saber de que tecla se trata. class MyEventReceiver : public IEventReceiver { // Se utiliza éste arreglo para verificar el estado de cada tecla bool KeyIsDown[KEY_KEY_CODES_COUNT]; public: // Este es el método que se debe implementar virtual bool OnEvent(const SEvent& event) { // Verificar si una tecla está siendo presionada if (event.EventType == irr::EET_KEY_INPUT_EVENT) KeyIsDown[event.KeyInput.Key] = event.KeyInput.PressedDown; return false; } // Ésta es utilizada para verificar si la tecla está siendo presionada virtual bool IsKeyDown(EKEY_CODE keyCode) const { return KeyIsDown[keyCode]; } MyEventReceiver() { for (u32 i=0; i> i; switch(i) { case 'a': driverType = video::EDT_DIRECT3D9;break; case 'b': driverType = video::EDT_DIRECT3D8;break; case 'c': driverType = video::EDT_OPENGL; break; case 'd': driverType = video::EDT_SOFTWARE; break;

38

case 'e': driverType = video::EDT_BURNINGSVIDEO;break; case 'f': driverType = video::EDT_NULL; break; default: return 0; } // Crear dispositivo MyEventReceiver receiver; IrrlichtDevice* device = createDevice( driverType, core::dimension2d(640, 480), 16, //bits de color false, //fullscreen false, //stencil buffer false, //vsync &receiver //manejador de eventos ); if (device == 0) return 1; // No se pudo crear el dispositivo. video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager();

Ahora se crea el nodo que será movido (una esfera) cuando las teclas 'W' y 'S' sean presionadas. La esfera forma parte de las primitivas geométricas incluidas en el Engine. Se traslada el nodo a la posición (0, 0, 30) y para hacerlo un poco más interesante, se le asigna una textura. También se desactivará la iluminación para cada modelo porque no hay ninguna luz dinámica en la escena (de otra forma los modelos se verían completamente negros). scene::ISceneNode * node = smgr->addSphereSceneNode(); if (node) { node->setPosition(core::vector3df(0,0,30)); node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp")); node->setMaterialFlag(video::EMF_LIGHTING, false); }

Ahora se creará otro nodo que se mueva por medio de un objeto animador (animator). Los nodos de escena de animación modifican a los nodos de escena y pueden ser atachados a cualquier nodo de escena, tal como un nodo de escena de malla, billboards, luces e incluso cámaras. Los nodos de animación no modifican únicamente la posición de un objeto, también puede por ejemplo, animar la textura de un nodo de escena. Ahora se creará un cubo y a éste se atacha el animator fly circle (volar en círculos), permitiendo a este nodo volar alrededor de la esfera. scene::ISceneNode* n = smgr->addCubeSceneNode(); if (n) { n->setMaterialTexture(0, driver->getTexture("../../media/t351sml.jpg")); n->setMaterialFlag(video::EMF_LIGHTING, false); scene::ISceneNodeAnimator* anim = smgr->createFlyCircleAnimator(

39

core::vector3df(0,0,30), //centro 20.0f //radio ); if (anim) { n->addAnimator(anim); anim->drop(); //liberar memoria } }

El último nodo de escena se agregará para mostrar las posibilidades de los animators sobre modelos MD2, ahora se usa el animator fly straight (volar en línea recta), éste permite realizar la animación del modelo corriendo entre dos puntos. scene::IAnimatedMeshSceneNode* anms = smgr->addAnimatedMeshSceneNode( smgr->getMesh("../../media/sydney.md2") ); if (anms) { scene::ISceneNodeAnimator* anim = smgr->createFlyStraightAnimator( core::vector3df(100,0,60), //origen core::vector3df(-100,0,60), //destino 2500, //tiempo de la animación true //loop ); if (anim) { anms->addAnimator(anim); anim->drop(); }

Para ver el modelo correctamente se desactiva la iluminación, también se establece el rango de fotogramas en el cual se realizará la animación del modelo, se rota 180 grados y se ajustan tanto la velocidad de animación como la textura. Para establecer la animación correcta, esto es: valores correctos de fotorgramas inicial y final y velocidad de la animación, se debe llamar a anms->setMD2Animation(scene::EMAT_RUN) para la animación de correr en lugar de llamar a setFrameLoop y setAnimationSpeed, pero ésto solo funciona con animaciones de modelos MD2. anms->setMaterialFlag(video::EMF_LIGHTING, false); anms->setFrameLoop(160, 183); anms->setAnimationSpeed(40); anms->setMD2Animation(scene::EMAT_RUN); anms->setRotation(core::vector3df(0,180.0f,0)); anms->setMaterialTexture(0, driver->getTexture("../../media/sydney.bmp")); }

Para poder ver y desplazarse en la escena, se agrega una cámara de tipo FPS y también es recomendable ocultar el puntero del mouse. 40

scene::ICameraSceneNode * cam = smgr->addCameraSceneNodeFPS(0, 100.0f, 100.0f); device->getCursorControl()->setVisible(false);

Agregar un logo de Irrlicht a todo color. device->getGUIEnvironment()->addImage( driver->getTexture("../../media/irrlichtlogoalpha2.tga"), //logo core::position2d(10,10) //posición );

Ahora la escena está lista, solo resta dibujar. También se desplegará la cantidad actual de fotogramas por segundo y el nombre del manejador de video en el título de la ventana. int lastFPS = -1; while(device->run()) {

Se debe verificar si alguna de las teclas W o S está siendo presionada, y si esto ocurre la esfera se moverá hacia arriba o abajo respectivamente. if(receiver.IsKeyDown(irr::KEY_KEY_W)) { core::vector3df v = node->getPosition(); v.Y += 0.02f; node->setPosition(v); } else if(receiver.IsKeyDown(irr::KEY_KEY_S)) { core::vector3df v = node->getPosition(); v.Y -= 0.02f; node->setPosition(v); } driver->beginScene( true, //backbuffer true, //z-buffer video::SColor( 255, //alfa 113, //rojo 113, //verde 133 //azul ) ); smgr->drawAll(); // Dibujar la escena 3D device->getGUIEnvironment()->drawAll(); // Dibujar el entorno GUI (El logo) driver->endScene(); int fps = driver->getFPS(); //frames per second if (lastFPS != fps) { core::stringw tmp(L"Movement Example - Irrlicht Engine ["); tmp += driver->getName(); tmp += L"] fps: ";

41

tmp += fps; device->setWindowCaption(tmp.c_str()); lastFPS = fps; } } //while

Para finalizar, se elimina el dispositivo. device->drop(); return 0; } //main

Compilar y probar. 2.6 Graphical User Inrface (GUI) A continuación se aborda en detalle cómo crear interfaces de usuario con el Engine Irrlicht. Se explica la forma en la que se crean y usan las ventanas, botones, barras de desplazamiento, textos estáticos y listas. Como siempre, se agregan los archivos de cabecera y se habilitan los nombres de espacio, además se crea un putero para el dispositivo Irrlicht, un contador para cambiar la posición de las ventanas cuando son creadas y un puntero para una lista (listbox). #include #include using namespace irr; using namespace core; using namespace scene; using namespace video; using namespace io; using namespace gui; #ifdef _IRR_WINDOWS_ #pragma comment(lib, "Irrlicht.lib") #endif IrrlichtDevice *device = 0; s32 cnt = 0; IGUIListBox* listbox = 0;

El manejador de eventos, además de capturar los eventos de teclado y mouse, también maneja los eventos de la interfaz gráfica de usuario (GUI). Hay eventos definidos para casi cualquier cosa: clic sobre botón, cambio en la selección de una lista, eventos que indican que un elemento tiene el puntero encima, etc. Para responder a algunos eventos se debe crear un manejador de eventos, por ahora solo se necesitará responder

42

a eventos GUI de la siguiente forma: se obtiene el ID del elemento que ha causado el evento y un puntero al entorno GUI. class MyEventReceiver : public IEventReceiver { public: virtual bool OnEvent(const SEvent& event) { if (event.EventType == EET_GUI_EVENT) { s32 id = event.GUIEvent.Caller->getID(); IGUIEnvironment* env = device->getGUIEnvironment(); switch(event.GUIEvent.EventType) {

Si una barra de desplazamiento ha cambiado de valor, y corresponde con el id 104, entonces se establece el valor de transparencia de todos los elementos GUI. Ésta es una tarea fácil. Existe un objeto llamado skin en el cual están almacenadas todas las configuraciones de color, solo es necesario cambiar el valor de alpha (transparencia). case EGET_SCROLL_BAR_CHANGED: if (id == 104) { s32 pos = ((IGUIScrollBar*)event.GUIEvent.Caller)->getPos(); for (u32 i=0; igetSkin()->getColor((EGUI_DEFAULT_COLOR)i); col.setAlpha(pos); env->getSkin()->setColor((EGUI_DEFAULT_COLOR)i, col); } } break;

Si un botón ha sido presionado, podría ser cualquiera de los tres botones que se añadirán a la ventana. Si es el primero, se cerrará la aplicación; si es el segundo, se muestra una pequeña ventana con algún mensaje y se agrega una cadena a la lista (listbox) para registrar lo que ha ocurrido. Por último, si es el tercer botón, se crea un cuadro de diálogo para abrir un archivo y se agrega también una cadena al listbox. Eso es todo para el manejador de eventos. case EGET_BUTTON_CLICKED: if (id == 101) { device->closeDevice(); return true; } if (id == 102) { listbox->addItem(L"Window created"); cnt += 30; if (cnt > 200) cnt = 0; IGUIWindow* window = env->addWindow( rect(100 + cnt, 100 + cnt, 300 + cnt, 200 + cnt), false, // modal? L"Test window"

43

); env->addStaticText( L"Please close me", rect(35,35,140,50), true, // border? false, // wordwrap? window ); return true; } if (id == 103) { listbox->addItem(L"File open"); env->addFileOpenDialog(L"Please choose a file."); return true; } break; default: break; } } return false; } };

Ahora se debe crear el dispositivo Irrlicht. Como en los ejemplos anteriores, el usuario determinará cuál de los controladores de video se va a utilizar. int main() { // Elegir driver video::E_DRIVER_TYPE driverType; printf("Por favor elija el controlador de video a utilizar:\n"\ " (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\ " (d) Software Renderer\n (e) Burning's Software Renderer\n"\ " (f) NullDevice\n (otherKey) exit\n\n"); char i; std::cin >> i; switch(i) { case 'a': driverType = video::EDT_DIRECT3D9;break; case 'b': driverType = video::EDT_DIRECT3D8;break; case 'c': driverType = video::EDT_OPENGL; break; case 'd': driverType = video::EDT_SOFTWARE; break; case 'e': driverType = video::EDT_BURNINGSVIDEO;break; case 'f': driverType = video::EDT_NULL; break; default: return 1; } // Crear el dispositivo o en su defecto terminar la ejecución.

44

device = createDevice(driverType, core::dimension2d(640, 480)); if (device == 0) return 1; // no se pudo crear el dispositivo.

Se ha creado satisfactoriamente, ahora se establece el manejador de eventos y se crean los punteros al driver y al entorno GUI. MyEventReceiver receiver; device->setEventReceiver(&receiver); device->setWindowCaption(L"Irrlicht Engine - User Interface Demo"); video::IVideoDriver* driver = device->getVideoDriver(); IGUIEnvironment* env = device->getGUIEnvironment();

Para hacer que el texto se vea un poco más interesante, se carga una fuente externa y se establece como la fuente por defecto del skin. Para conservar la fuente por defecto en los tool tips (globos de ayuda o información adicional que aparece al dejar el puntero por unos instantes sobre algún elemento GUI) se establece la fuente incluída en el Engine. IGUISkin* skin = env->getSkin(); IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp"); if (font) skin->setFont(font); skin->setFont(env->getBuiltInFont(), EGDF_TOOLTIP);

Ahora se agregarán los botones, el primero cierra el Engine, el segundo crea una ventana con un mensaje y el tercero abre el cuadro de diálogo de abrir archivo. El tercer parámetro del método ->addButton es el ID del botón, éste es utilizado para identificar fácilmente desde el manejador de eventos cuál de los botones ha sido presionado. Los otros parámetros de dicho método están definidos en los comentarios del código siguiente: env->addButton( rect(10,240,110,240 + 32), // posicionamiento 0, // padre 101, //ID L"Terminar", //Etiqueta L"Sale del programa" //Tooltip ); env->addButton( rect(10,280,110,280 + 32), 0, 102, L"Nueva ventana", L"Crea una nueva ventana" );

45

env->addButton( rect(10,320,110,320 + 32), 0, 103, L"Abrir Archivo", L"Abre un archivo" );

Ahora se agregará un texto estático y una barra de desplazamiento que modifique la opacidad de todos los elementos GUI. Se establece 255 como valor máximo, porque es el valor más alto utilizado en colores (1 byte). Luego se crea otro texto estático y el listbox. env->addStaticText(L"Transparent Control:", rect(150,20,350,40), true); IGUIScrollBar* scrollbar = env->addScrollBar( true, //horizontal rect(150, 45, 350, 60), //dimensiones 0, //parent 104 //ID ); scrollbar->setMax(255); // Establecer el valor de la barra de deslizamiento igual al valor alpha de // un elemento elegido arbitrariamente. scrollbar->setPos(env->getSkin()->getColor(EGDC_WINDOW).getAlpha()); env->addStaticText(L"Registro de sucesos:", rect(50,110,250,130), true); listbox = env->addListBox(rect(50, 140, 250, 210)); env->addEditBox(L"Texto editable", rect(350, 80, 550, 100));

Por último se agrega una imagen del logo del Engine en la esquina superior izquierda. env->addImage( driver->getTexture("../../media/irrlichtlogo2.png"), position2d(10,10) );

Por ahora solo resta dibujar la escena. while(device->run() && driver) if (device->isWindowActive()) { driver->beginScene(true, true, SColor(0,200,200,200)); env->drawAll(); driver->endScene(); } device->drop(); return 0; } //main

46

Al compilar y ejecutar la aplicación se verá una ventana como la que se muestra en la figura 2.6

Figura 2.6. Algunos elementos de la interfaz gráfica de usuario incluida en el Engine Irrlicht.

2.7 Detección de colisiones A continuación se mostrará como detectar colisiones con el Engine Irrlicht, en esta sección se abarcan tres métodos: (1) Detección automática de colisiones para moverse por mundos tridimensionales con la capacidad para subir gradas y deslizarse, (2) selección manual de triángulos y (3) selección de nodos de escena. Para iniciar se utilizará el programa de la Sección 1.4 (Cargar mapas de Quake 3 y agregar una cámara FPS), el cual carga un nivel de Quake 3. Se utilizará dicho nivel para caminar en él y para tomar sus triángulos. Adicionalmente se agregan tres modelos animados a la escena. El siguiente código inicia el Engine y carga el mapa de Quake 3. Esto ya fue expuesto en la sección 1.4. #include #include using namespace irr; #ifdef _MSC_VER #pragma comment(lib, "Irrlicht.lib") #endif int main() { // let user select driver type video::E_DRIVER_TYPE driverType; printf("Por favor seleccione el driver a utilizar:\n"\ " (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\ " (d) Software Renderer\n (e) Burning's Software Renderer\n"\ " (f) NullDevice\n (otherKey) exit\n\n");

47

char i; std::cin >> i; switch(i) { case 'a': driverType = video::EDT_DIRECT3D9;break; case 'b': driverType = video::EDT_DIRECT3D8;break; case 'c': driverType = video::EDT_OPENGL; break; case 'd': driverType = video::EDT_SOFTWARE; break; case 'e': driverType = video::EDT_BURNINGSVIDEO;break; case 'f': driverType = video::EDT_NULL; break; default: return 0; } // Crear el dispositivo IrrlichtDevice *device = createDevice( driverType, core::dimension2d(640, 480), 16, False ); if (device == 0) return 1; // no se pudo crear el driver seleccionado. video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager(); device->getFileSystem()->addZipFileArchive("../../media/map-20kdm2.pk3"); scene::IAnimatedMesh* q3levelmesh = smgr->getMesh("20kdm2.bsp"); scene::ISceneNode* q3node = 0; if (q3levelmesh) q3node = smgr->addOctTreeSceneNode(q3levelmesh->getMesh(0));

Ahora algo diferente, crear el selector de triángulos. Un selector de triángulos o triangle selector es una clase con la que se pueden extraer los triángulos de nodos de escena para realizar diferentes cosas con ellos, por ejemplo: detección de colisiones. Hay diferentes selectores, y todos pueden ser creados con la ISceneManager. En este ejemplo se crea un OctTreeTriangleSelector, el cual optimiza un poco la salida de triángulos, reduciéndolo como un octree8, esto es muy útil con mallas enormes como los niveles de Quake 3. Después de que se ha creado el selector se atacha al objeto q3node. Esto no es realmente necesario pero de esta forma, no será necesario encargarse del selector por separado, por ejemplo para liberarlo cuando ya no se necesita más. scene::ITriangleSelector* selector = 0; if (q3node) {

8

Véase el término octree en el glosario.

48

q3node->setPosition(core::vector3df(-1350,-130,-1400)); selector = smgr->createOctTreeTriangleSelector( q3levelmesh->getMesh(0), q3node, 128 ); q3node->setTriangleSelector(selector); }

Ahora se agrega una cámara de tipo FPS a la escena para poder moverse a través del nivel de Quake 3. En esta ocasión, se agregará un animator especial a la cámara, que responda a colisiones (collision response animator). Esto modifica el nodo de escena al cual el animator ha sido atachado, de manera que el nodo de escena no puede moverse a través de las paredes del entorno y es afectado por la gravedad. Lo único que se debe indicar al animator es la forma que tiene todo el entorno, qué tan grande es el nodo de escena, el valor de la gravedad, etc. Luego de que el animator de respuesta a colisiones es atachado a la cámara, no se tendrá que hacer nada extra para la detección de colisiones, todo es hecho automáticamente. El animator de respuesta a colisiones también puede ser atachado a todos los otros nodos de escena, no solamente a cámaras, incluso puede ser combinado con otros animators. Ahora se examinan detenidamente los parámetros de createCollisionResponseAnimator(). El primer parámetro es el TriangleSelector, el cual especifica cómo “se ve el mundo”. El segundo parámetro es el nodo de escena, éste es el objeto que será afectado por la detección de colisiones, la cámara. El tercer parámetro define cuán grande es el objeto por medio de un elipsoide9 (definiendo sus tres radios). Al reducir los radios del elipsoide la cámara puede acercarse más a las paredes. El cuarto parámetro representa la dirección y velocidad de la gravedad. Puede establecer este valor a (0,0,0) para desactivar la gravedad. El quinto y último parámetro es sólo una traslación, sin esto la cámara quedaría justo en el centro del elipsoide, de manera que se colocará la cámara 50 unidades arriba del centro del elipsoide; con esto se hará funcionar la detección de colisiones. scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS(0, 100.0f, 300.0f, -1, 0, 0, true); camera->setPosition(core::vector3df(-100,50,-150)); if (selector) { scene::ISceneNodeAnimator* anim = smgr->createCollisionResponseAnimator( 9

Un elipsoide en matemáticas, es una cuádrica (ver glosario) análoga a la elipse, pero con una dimensión más. Las secciones originadas por planos que contienen a los ejes ortogonales son elípticas. La ecuación general de un elipsoide con centro en el origen de coordenadas, es: números reales positivos que determinan la forma del elipsoide.

49

donde a, b y c son

selector, //triangle selector camera, //Nodo de escena core::vector3df(30,50,30), //Radios del elipsoide core::vector3df(0,-3,0), //Gravedad core::vector3df(0,50,0) //Pos. Rel. del nodo al elipsoide. ); selector->drop(); camera->addAnimator(anim); anim->drop(); }

Ahora se agregan tres modelos animados, una luz dinámica para iluminarlos, un billboard para mostrar la intersección entre el puntero y el selector, además de ocultar el puntero del mouse. // Esconder el puntero del mouse device->getCursorControl()->setVisible(false); // Agregar el billboard scene::IBillboardSceneNode * bill = smgr->addBillboardSceneNode(); bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR ); bill->setMaterialTexture(0, driver->getTexture("../../media/particle.bmp")); bill->setMaterialFlag(video::EMF_LIGHTING, false); bill->setMaterialFlag(video::EMF_ZBUFFER, false); bill->setSize(core::dimension2d(20.0f, 20.0f)); // Cargar tres adas animadas. video::SMaterial material; material.setTexture(0, driver->getTexture("../../media/faerie2.bmp")); material.Lighting = true; scene::IAnimatedMeshSceneNode* node = 0; scene::IAnimatedMesh* faerie = smgr->getMesh("../../media/faerie.md2"); if (faerie) { node = smgr->addAnimatedMeshSceneNode(faerie); node->setPosition(core::vector3df(-70,0,-90)); node->setMD2Animation(scene::EMAT_RUN); node->getMaterial(0) = material; node = smgr->addAnimatedMeshSceneNode(faerie); node->setPosition(core::vector3df(-70,0,-30)); node->setMD2Animation(scene::EMAT_SALUTE); node->getMaterial(0) = material; node = smgr->addAnimatedMeshSceneNode(faerie); node->setPosition(core::vector3df(-70,0,-60)); node->setMD2Animation(scene::EMAT_JUMP); node->getMaterial(0) = material; } material.setTexture(0, 0);

50

material.Lighting = false; // Agregar un luz dinámica smgr->addLightSceneNode( 0, //padre core::vector3df(-60,100,400), //posición video::SColorf(1.0f,1.0f,1.0f,1.0f), //color RGBA 600.0f //radio de iluminación );

Para simplificar el proceso de detección de triángulos, se va a realizar la operación dentro del ciclo de dibujo. Se crean dos punteros para almacenar el actual y el último nodos de escena e iniciar el ciclo. scene::ISceneNode* selectedSceneNode = 0; scene::ISceneNode* lastSelectedSceneNode = 0; int lastFPS = -1; while(device->run()) if (device->isWindowActive()) { driver->beginScene(true, true, 0); smgr->drawAll();

Ahora se describirán las otras dos formas de hacer detección de colisiones. Después de haber dibujado la escena completa con smgr->drawAll(), se realiza la primera detección, se desea saber cuál de los triángulos que conforman la malla del escenario está apuntando la cámara y el punto exacto donde se intersecta la línea de visión con dicha malla. Para esto se crea una línea 3D (línea de visión), iniciando en la posición de la cámara y dirigiéndola hasta el punto donde la cámara está viendo (camera>getTarget()), luego se verifica en el manejador de colisiones si esta línea toca alguno de los triángulos del escenario guardados en el selector. Si es así, se dibuja un triángulo 3D que corresponde al triánguno detectado en la malla y se establece la posición del billboard en el punto de intersección. core::line3d line; line.start = camera->getPosition(); line.end = line.start + (camera->getTarget() - line.start).normalize() * 1000.0f; core::vector3df intersection; core::triangle3df tri; if (smgr->getSceneCollisionManager()->getCollisionPoint( line, selector, intersection, tri)) { bill->setPosition(intersection);

51

driver->setTransform(video::ETS_WORLD, >setTransform(video::ETS_WORLD, core::matrix4()); driver->setMaterial(material); rial(material); driver->draw3DTriangle(tri, >draw3DTriangle(tri, video::SColor(0,255,0,0)); }

Otra a forma de detectar un nodo visible desde la cámara se basa en las cajas delimitadoras (o bounding box) box de los nodos. Véase la figura 2.7. Cada nodo de escena tiene una caja delimitadora y en consecuencia resulta bastante rápido determinar qué nodo de escena está diréctamen diréctamente frente a la cámara. De nuevo, se verifica esto en el manejador de colisiones, utilizando el método ->getSceneNodeFromCameraBB(camera). CameraBB(camera). Si dicho método devuelve un puntero a nodo de escena exceptuando los objetos billboard y q3node, se procederá a desactivar la propiedad Lighting en su material.

Figura 2.7. La figura muestra el nodo de escena de un pato, delimitado por su bonding box. selectedSceneNode = smgr-> > getSceneCollisionManager()-> getSceneNodeFromCameraBB(camera); if (lastSelectedSceneNode) lastSelectedSceneNode lastSelectedSceneNode-> setMaterialFlag(video::EMF_LIGHTING, true); if (selectedSceneNode == q3node || selectedSceneNode == bill) selectedSceneNode = 0; if (selectedSceneNode) Node) selectedSceneNode selectedSceneNode->setMaterialFlag( video::EMF_LIGHTING, false ); lastSelectedSceneNode = selectedSceneNode;

Para finalizar, se dibuja la escena. driver->endScene();

52

int fps = driver->getFPS(); if (lastFPS != fps) { core::stringw str = L"Colisiones - Irrlicht Engine ["; str += driver->getName(); str += "] FPS:"; str += fps; device->setWindowCaption(str.c_str()); lastFPS = fps; } } device->drop(); return 0; }

2.8 Audio 2D y 3D con irrKlang IrrKlang es una poderosa API de alto nivel para controlar la reproducción de sonidos en aplicaciones 3D y 2D como juegos, programas científicos de visualización y aplicaciones multimedia. Es un Engine de sonido 2D y 3D y librería de audio multiplataforma y se puede utilizar con C++ y todos los lenguajes de la plataforma .NET (C#, VisualBasic.NET, etc.). Tiene características incorporadas muy útiles como decodificadores de formatos de audio (wav, ogg, mp3, mod, xm, it, s3m...), un sofisticado mecanismo de streaming 10 , lectura de audio extensible, trabajo en modo simple y multihilo, capacidad para emular audio 3D, un sistemas de plugins (complementos o extensiones), múltiples modelos de rollof11 y más. Además es libre (open source) y puede ser utilizado para propósitos no comerciales. 2.8.1 EJECUTANDO SONIDOS (2D) A continuación se demuestra cómo ejecutar sonidos utilizando IrrKlang. Se ejecutará un sonido de fondo que se repite indefinidamente y un segundo sonido cada vez que se presiona una tecla. El SDK se puede descargar de: http://www.ambiera.com/irrklang/downloads.html, luego de instalado se agregan al proyecto los directorios lib\Win32-visualStudio e include, contenidos en el directorio de instalación del SDK, es un procedimiento muy similar al descrito en la sección 1.3 / Configurando el IDE. Los ejemplos y demás archivos necesarios para su implementación vienen también incluidos en el SDK en el directorio examples.

10 11

Más información sobre streaming en el glosario. Vea el término rollof en el glosario.

53

Se procede, desde Visual Studio a crear un proyecto vacío (aplicación de consola). Para comenzar se deben incluir los archivos de cabecera. #include #include

Adicionalmente de la forma en que se declaran los nombres de espacio con Irrlicht, se debe indicar al compilador que se usará el nombre de espacio irrklang. Todas las clases y funciones de irrKlang se encuentran dentro del nombre de espacio irrklang. Para utilizar cualquier clase del Engine, se debe escribir irrklang:: antes del nombre de la clase. Por ejemplo, para usar la clase ISoundEngine, se escribe: irrklang::ISoundEngine. Para evitar escribir irrklang:: siempre que se usa una clase, se agregará instrucción using namespace irrklang. using namespace irrklang;

Para poder acceder al archivo irrKlang.dll, se necesita vincular la aplicación con el archivo irrKlang.lib. Para ello basta con una instrucción pragma comment: #pragma comment(lib, "irrKlang.lib") // Vincular con irrKlang.dll

Para iniciar, se demostrará una simple ejecución de audio 2D. Se inicializa el Engine de sonido usando el método createIrrKlangDevice(). Dicho método puede requerir varios parámetros, por ahora bastará con los parámetros por defecto. int main(int argc, const char** argv) { // Inicializar el Engine de sonido con los parámetros por defecto ISoundEngine* Engine = createIrrKlangDevice(); if (!Engine) return 0; // error al iniciar el Engine

Para ejecuar un sonido simple, se utiliza el método play2D(). El primer parámetro hace referencia al archivo de sonido a ejecutar, el segundo parámetro de este método le indica al Engine que el sonido se va a ejecutar indefinidamente (al finalizar la reproducción inicia de nuevo). // Ejecutar el stream de audio, indefinidamente. Engine->play2D("../../media/getout.ogg", true);

El programa se ejecutará en un ciclo hasta que se presione la tecla q para salir de la aplicación o cualquier otra tecla para ejecutar el segundo archivo de sonido. std::cout > i; // esperar a que el usuario presione una tecla. }

Al finalizar el ciclo, se debe eliminar el dispositivo IrrKlang que se ha creado previamente con createIrrKlangDevice(), pare ello se utiliza el método drop(). En IrrKlang, al igual que en Irrlicht se deben eliminar todos los objetos que se hayan creado con un método o función que inicie con create. (play2D() y play3D() son excepciones a esta regla. Para ampliar esta información consulte la documentación12). Engine->drop(); // eliminar el Engine return 0; }

Sólo resta compilar y ejecutar la aplicación. 2.8.2 SONIDO 3D Cuando un sonido se ejecuta en un punto en particular del espacio, el cual tiene una distancia relativa al oyente, se denomirá sonido 3D. Esto puede lograrse gracias a los sistemas de audio estéreo de dos o más canales (Equipos de audio con dos o más bocinas). De modo que el sonido puede ejecutarse con un determinado volumen en cada una de las bocinas para simular cierto grado de cercanía desde el origen del sonido hacia el oyente. Por ejemplo el sonido puede trasladarse gradualmente de una bocina a la próxima, simulando el movimiento del origen del mismo. Véase la figura 2.8.

Figura 2.8. En la figura se muestra un sistema de audio con cinco bocinas, se está simulando que un objeto emisor de sonido se mueve alrededor del oyente. Como se puede apreciar, solamente algunas de las bocinas están activas. 12

irrKlang 1.1.0 API documentation: http://www.ambiera.com/irrklang/docu/index.html

55

A continuación se mostrará cómo ejecutar sonidos en el espacio 3D, utilizando irrKlang. Un archivo de audio mp3 será ejecutado en el espacio 3D y se moverá alrededor del usuario y otro sonido se ejecutará en una posición 3D aleatoria cada vez que se presione una tecla. Se necesita una función para dormir el programa por algunos segundos, entónces se incluirán los archivos específicos de la plataforma para poder acceder a dicha función. #ifdef WIN32 #include #include inline void sleepSomeTime() { Sleep(100); } #endif

También se incluyen los archivos de cabecera de entrada/salida necesarios para imprimir y obtener etradas de usuario desde la consola. #include #include #include using namespace irrklang; #pragma comment(lib, "irrKlang.lib") // link with irrKlang.dll

A continuación se inicializa el Engine utilizando createIrrKlangDevice() con las opciones o parámetros por defecto. int main(int argc, const char** argv) { // Iniciar el enginde de sonido con los parámetros por defecto ISoundEngine* Engine = createIrrKlangDevice(); if (!Engine) return 0; // error starting up the Engine

Ahora se ejecutará indefinidamente el stream de sonido como música en el espacio 3D, se establecerá el último parámetro llamado 'track' a 'true' para hacer que irrKlang retorne un puntero al sonido ejecutado. (Este puntero también es devuelto si el parámetro 'startPaused' está definido como true). Notese que es neceario llamar al método ->drop del puntero retornado si ya no se necesita usar este sonido más adelante. Esto se hace al final del programa. // Ejecutar un sonido en el espacio ISound* music = Engine->play3D( "../../media/ophelia.mp3", //Archivo de sonido vec3df(0,0,0), // Posición true, //ejecutar indefinidamente (looped) false, //iniciar pausado (startPaused)

56

true //retornar un puntero al sonido para poder controlarlo (track) );

El siguiente paso no es realmente necesario, pero, para ajustar la distancia a la cual el sonido puede ser oído se definirá una distancia mínima (el valor por defecto es 1 para objetos pequeños). La distancia mínima es simplemente la distancia a la cual el sonido es ejecutado con su volumen máximo. if (music) music->setMinDistance(5.0f);

Imprimir un texto de ayuda e iniciar el ciclo: printf("\nEjecutando sonido en 3D."); printf("\nPresiones ESCAPE para salir, cualquier otra tecla "\ "para ejecutar otro sonido en una posición aleatoria.\n\n"); printf("+ = Posición de quien escucha\n"); printf("o = Ejecutando sonido\n"); float posOnCircle = 0; const float radius = 5; while(true) { // ciclo infinito, hasta que el usuacio salga

Para cada paso por el ciclo se calcula la posición de la música en el espacio, en este ejemplo se hará que la música rote su posición sobre una circunferencia. posOnCircle += 0.04f; vec3df pos3d(radius * cosf(posOnCircle), // X 0, // Y radius * sinf(posOnCircle * 0.5f) // Z );

Después de calcular la posición de la música se debe indicar a irrKlang la posición de quien escucha (en este caso (0,0,0), con dirección hacia adelante). Engine->setListenerPosition(vec3df(0,0,0), //posición del oyente (listener) vec3df(0,0,1)); //dirección (lookdir) if (music) music->setPosition(pos3d);

Ahora se imprimirá la posición del sonido representada en una cadena y también un medidor de avance de reproducción. char stringForDisplay[] = "

+

";

int charpos = (int)((pos3d.X + radius) / radius * 10.0f); if (charpos >= 0 && charpos < 20)

57

stringForDisplay[charpos] = 'o'; int playPos = music ? music->getPlayPosition() : 0; printf("\rx:(%s) 3dpos: %.1f %.1f %.1f, playpos:%d:%.2d stringForDisplay, pos3d.X, pos3d.Y, pos3d.Z, playPos/60000, (playPos%60000)/1000 );

",

sleepSomeTime();

Manejo de teclado: Cada vez que el usuario presiona una tecla en la consola se ejecuta un sonido aleatorio o sale de la aplicación si se ha presionado ESC. if (kbhit()) { //si se ha presionado una tecla int key = getch(); //obtener la tecla if (key == 27) // Código para la tecla ESC break; else {

Ahora se procede a ejecutar el sonido en una posición aleatoria. El método play3D() no retorna ningún puntero (devuelve 0) porque no se ha establecido a true ninguno de los parámetros startPaused (iniciar pausado) o track (como se hizo anteriormente con la múscia). Por esta razón no es necesario llamar a drop(). vec3df pos(fmodf((float)rand(),radius*2)-radius, 0, 0); //posición aleatoria const char* filename; if (rand()%2) //se elige un sonido al azar filename = "../../media/bell.wav"; else filename = "../../media/explosion.wav"; Engine->play3D(filename, pos); //ejecutar filename en la posición pos printf("\nreproduciendo %s en %.1f %.1f %.1f\n", filename, pos.X, pos.Y, pos.Z);

Por ahora solo resta liberar los recursos cuando el programa finaliza. } } } // No olvide liberar los recursos como se explicó. if (music) music->drop(); // release music stream. Engine->drop(); // eliminar el Engine return 0;

58

}

Además de permitir ejecutar sonido 2D y 3D, irrKlang incorpora una serie de efectos especiales que pueden aplicarse a cualquier sonido. Entre ellos se incluye el Efecto Doppler 13 , coro, compresión, distorsión, echo, flanger 14 , gargling 15 , reverb 16 y efectos de ecualización paramétrica, entre otros. Puede obtenerse una completa referencia de la API de IrrKlang, tutoriales y otros recursos en http://www.ambiera.com/irrklang 2.9 irrXML irrXML es un parser o analizador sintáctico para leer archivos XML con datos no validados (asume que están sintácticame correctos de acuerdo al lenguaje XML), es rápido y simple, de código abierto, escrito en C++ y compatible con lenguajes .NET. Este parser de XML viene incluido en el Engine Irrlicht pero también puede obtenerse por separado para usarlo en cualquier tipo de aplicación. Más información en: http://www.ambiera.com/irrxml Si se usa de forma independiente simplemente se debe agregar al proyecto el directorio /src incluído en el SDK de irrXML. Para usuarios Linux / Unix se debe ir al directorio /examples y ejecutar la instrucción ‘make’ en la consola, esto creará un proyecto simple que incluye irrXML. A continuación se describe un breve ejemplo que muestra como acceder a un archivo XML: config.xml. Bienvenido al visor de mallas de "Irrlicht Engine".

El programa que leerá el archivo XML simplemente guarda los datos obtenidos en las variables messageText, modelFile y Caption. Luego de obtenida la información se puede proceder a realizar cualquier acción, por ejemplo, desplegarla en pantalla. 13

Efecto Doppler: Consiste en la variación de la longitud de onda de cualquier tipo de onda emitida o recibida por un objeto en movimiento y que se produce a causa del mismo movimiento. 14 Efecto flanger: Consiste en mezclar la señal original de audio con una copia retardada en el tiempo, con la particularidad que el retardo es muy breve pero varía de forma periódica. 15 Efecto gargling: Consiste variar la señal de audio de forma que suene como si se hicieran “gárgaras”. 16 Efecto reverb: Este es el efecto que se aplica al material de audio para dar al oyente la impresión que está en otro cuarto.

59

#include using namespace irr; // localizar irrXML. using namespace io; // usando el nombre de espacio irr::io 17

#include // Usamos cadenas STL // en este ejemplo.

para almacenar los datos

void main() { // Creamos el lector usando una de las funciones de creación IrrXMLReader* xml = createIrrXMLReader("config.xml"); // Algunas cadenas para almacenar los datos que obtenemos del archivo xml std::string modelFile; std::string messageText; std::string caption; // leer el archivo hasta que se encuentre el final del mismo. while(xml && xml->read()) { switch(xml->getNodeType()) { case EXN_TEXT: // En este archivo xml, el único texto se encuentra dentro de las // etiquetas y messageText = xml->getNodeData(); break; case EXN_ELEMENT: if (!strcmp("model", xml->getNodeName())) //etiqueta model modelFile = xml->getAttributeValue("file"); //atributo file else if (!strcmp("messageText", xml->getNodeName())) //etiqueta messageText caption = xml->getAttributeValue("caption"); //atributo caption break; } } // Liberar la memoria delete xml; }

17

STL o Standard Template Library es una librería de software parcialmente incluida en la librería estándar de C++. Provee contenedores, iteradores, algoritmos y funciones utilizados para manejar diferentes estructuras de datos de computadoras.

60

Capítulo III

RakNet

RakNet kNet es una librería para manejo de red basada en UDP. Está diseñada para permitir a los programadores agregar capacidad de respuesta en tiempo crítico a sus aplicaciones de red.

61

62

3.1 Introducción RakNet es una librería para manejo de red basada en UDP. Está diseñada para permitir a los programadores agregar capacidad de respuesta en tiempo crítico a sus aplicaciones de red. Es utilizada principalmente para videojuegos, pero es independiente de la aplicación que se le quiera dar. RakNet está diseñada para ser veloz, fácil de usar, independiente de la aplicación, independiente de la plataforma y flexible. RakNet está bajo licencias diferentes, de acuerdo al tipo de servicio o aplicación que se va a desarrollar. Para aplicaciones no comerciales aplica Creative Commons “Reconocimiento-No comercial 2.5 Genérica 18 ”. En el caso de aplicaciones simples para desarrolladores individuales aplica la licencia “Indy 19 ”. Para desarrolladores mayores, la licencia “Developer” y para Engines de juegos y aplicaciones grandes aplica “Publisher license”. Más información en: http://www.jenkinssoftware.com/purchase.html 3.2 Características

• • • •

• • • •

Entre las características más importantes se encuentran las siguientes: Lobby System: Es un plugin que provee funcionalidad controlada por una base de datos PostgreSQL para datos persistentes de juegos y matchmaking20. Sistema de replicación de objetos: Permite de manera automática crear, destruir, serializar21 y transmitir los objetos de la aplicación o juego en desarrollo. Conexiones Seguras22: Soporte para SHA1, AES128, SYN Cookies, y RSA para prevenir y detectar ataque de red. Robusta capa de comunicación: Control automático de flujo, ordenamiento de mensajes en múltiples canales, coalescence 23 de mensajes y segmentación y reemsamblado de paquetes. Autopatcher: Consiste en una clase que maneja la actualización de paquetes perdidos o modificados entre dos sistemas. Llamadas a procedimientos remotos: Puede llamar a procedimientos nativos de C y C++ con listas de parámetros automáticamente serializadas. Comunicación por voz: Incluye bindings de audio para Port Audio 24 , FMOD y DirectSound. NAT 25 Punchthrough: Si ambas máquinas se encuentran detrás de un NAT (router), es imposible para ellas conectarse entre sí, a menos que ambas conozcan la dirección pública y puerto de la otra. La clase NatPunchthrough

18

Creative Commons “Reconocimiento-No comercial 2.5 Genérica”, http://creativecommons.org/licenses/by-nc/2.5/deed.es 19 Indy License, http://www.jenkinssoftware.com/IndyLicense-1.0.pdf 20 Matchmaking: Término inglés que se refiere al proceso de ayudar a las personas a encontrar pareja. 21 Vea serialización en el glosario. 22 Para más información de cada uno de los términos SHA1, AES128, SYN Cookies, y RSA consulte el glosario. 23 Coalescence: Es la acción de fusionar dos bloques adyacentes de memoria libre. 24 Port Audio: Es una librería para grabar y reproducir audio. Es independiente de la plataforma y de código abierto. 25 NAT: Acrónimo de Network Address Translation. Mapea dirección IP públicas a direcciones IP locales.

63

implementa esta técnica permitiendo a ambos sistemas conectarse a través de un tercer sistema llamado facilitador, éste no se encuentra detrás de NAT y actúa como un intermediario en la comunicación. 3.3 Incluir RakNet en un proyecto26 Hay varias formas de incluir RakNet en un proyecto: puede utilizar el código fuente, una librería estática o bien una DLL. En este texto se explicará cómo incluir el código fuente de RakNet en un proyecto de Visual Studio 2005. La ventaja de hacerlo de esta manera es que permite que en cualquier momento se pueda acceder al código de todas las clases y sus métodos para conocer el órden de los parámetos, los atributos de clase, etc. El código fuente de RakNet incluye una amplia documentación interna con comentarios y descripciones detalladas. Lo primero que se debe hacer es agregar el directorio /Source al proyecto. No todos los archivos son estrictamente necesarios, pero los que no se usen tampoco estorbarán. Sin embargo, si no se planea utilizar RakVoice podrían excluirse todos los archivos que incluyan RakVoice en su nombre, de otra forma se deben incluir todos los archivos speex27.

• •

Si se necesita incluir RakVoice se debe agregar lo siguiente: Los archivos fuente contenidos en el directorio DependentExtensions\speexx.x.xx\libspeex. Agregar a los directorios include adicionales "Additional Include Directories" el lo siguiente: ..\..\..\Source;..\..\..\speex-1.1.4\libspeex.

A continuación se debe importar ws2_32.lib o bien wsock32.lib si no se tiene instalado Winsock 2. En VisaulStudio.NET se incluye haciendo clic derecho sobre el proyecto, luego se selecciona configuration properties (propiedades de configuración) > linker > input > additional dependencies y ahí se agrega el archivo "ws2_32.lib", como se vé en la figura 3.2.

Figura 3.2. Incluir ws2_32.lib en el proyecto (o en su defecto, incluya wsock32.lib) 26

Fuente: Compiler Setup, http://www.jenkinssoftware.com/raknet/manual/compilersetup.html El proyecto Speex tiene como objetivo crear un códec libre para voz, sin restricciones de ninguna patente de software. Speex está sujeto a la Licencia BSD y es usado con el contenedor Ogg de la Fundación Xiph.org. Fuente: http://es.wikipedia.org/wiki/Speex

27

64

También se debe configurar el proyecto para usar las librerías en tiempo de ejecución multi-hilo (multi-threaded runtime libraries). En VisualStudio.NET se debe hacer clic derecho sobre el proyecto, seleccionar las propiedades de configuración (configuration properties) > C/C++ > Code Generation > Runtime Library, y por último cambiar el valor a Multi-threaded (/MT), como se aprecia en la figura 3.3.

Figura 3.3. Establecer el proyecto para usar Multi-threaded en las librerías runtime. Opcionalmente se pueden configurar las directivas de preprocesador. Más información al respecto en: http://www.jenkinssoftware.com/raknet/manual/preprocessordirectives.html 3.4 Comunicación básica: envío de cadenas A continuación se ejemplificará el funcionamiento básico de RakNet, se crean dos proyectos incluyendo RakNet (tal y como se mostró en la sección anterior), un servidor y un cliente. Los clientes (instancias de la aplicación client) al momento de iniciar, solicitan la dirección IP del servidor y se conectan por el puerto 60000, luego de conectados envían cadenas de texto al servidor por medio de paquetes. Los archivos de proyecto se incluyen en el CD que acompaña a este material. 3.4.1 SERVIDOR (MAIN. CPP) Para comenzar se deben incluir los archivos de cabecera. El archivo stdio.h es para entrada y salida desde la consola. RakNetworkFactory.h contiene las clases de creación de todos los objetos de RakNet, RakPeerInterface.h es una interfaz de RakPeer que contiene todas las funciones de usuario definidas como virtuales puras 28 . El archivo MessageIdentifiers.h contiene todos los identificadores de mensajes utilizados por RakNet. Un identificador de mensaje es el número contenido en el primer byte de cada mensaje. #include #include "RakNetworkFactory.h" #include "RakPeerInterface.h" #include "MessageIdentifiers.h"

28

Las funciones virtuales puras definen la interfaz de una clase abstracta (que no se va a instanciar). La clase abstracta es simplemente una interfaz para todas las clases derivadas de ésta, las cuales pueden redefinir las funciones virtuales.

65

Ahora se define el número máximo de clientes y el puerto a utilizar para la comunicación #define MAX_CLIENTS 10 #define SERVER_PORT 60000

RakPeerInterface es la interfaz principal de RakNet, RakPeer contiene todas las funciones principales de la librería. Se crea un objeto llamado peer que permitirá controlar la comunicación. RakPeerInterface *peer;

Ahora se define un procedimiento para imprimir los mensajes recibidos. void PrintMessage(RPCParameters *rpcParameters) { printf("%s\n", rpcParameters->input); }

llama a una función de C en el sistema remoto que fue registrado usando RegisterAsRemoteProcedureCall(). Sus parámetros son: RakPeerInterface::RPC

• • • • • • •

uniqueID: Una cadena terminada en NULL que identifica la función a llamar. Es recomendable usar la macro CLASS_MEMBER_ID para funciones de clase. data: Los datos a enviar. bitLength: El número de bits de los datos. priority: Establecer el nivel de prioridad del envío. (Véase el archivo PacketPriority.h) reliability: Fiabilidad de los datos. (Véase el archivo PacketPriority.h) orderingChannel: Cuando se envían los mensajes en orden o secuencia, respecto de qué canal se debe ordenar. systemAddress: A quien envíar este mensaje o en el caso de usar broadcast a quien no envíar el mensaje. Para especificar ninguno use UNASSIGNED_SYSTEM_ADDRESS

• • •



broadcast: establecer a True para envíar el mensaje como broadcast. includedTimestamp: Pase un valor para ajustar el tiempo por defecto como ID_TIMESTAMP. 0 para no utilizar esta característica. networkID: Para funciones estáticas pase UNASSIGNED_NETWORK_ID. Para funciones miembro debe derivar de NetworkIDObject y pasar el valor retornado por NetworkIDObject::GetNetworkID para ese objeto. replyFromTarget: Si el valor es diferente de 0 se bloqueará hasta que reciba una respuesta desde el procedimiento de destino, el cual debe estar remotamente escrito por RPCParameters::replyToSender y copiado a replyFromTarget. peer->RPC( "PrintMessage", (const char*)rpcParameters->input,

66

rpcParameters->numberOfBitsOfData, HIGH_PRIORITY, RELIABLE_ORDERED, 0, rpcParameters->sender, true, 0, UNASSIGNED_NETWORK_ID, 0 ); }

Ahora en main(), se crea un paquete (packet) el cual se utilizará para recibir todos los mensajes que lleguen a la aplicación. Se crea la interfaz de comunicación en peer y se inicializa el servidor con StartUp. int main(void) { Packet *packet; peer = RakNetworkFactory::GetRakPeerInterface(); peer->Startup( MAX_CLIENTS, //Conexiones entrantes 30, Cuantos milisegundos debe esperar para cada ciclo de actualizacion &SocketDescriptor( //Local socket SERVER_PORT, //Puerto del servidor 0 //Dirección del host, 0 = local ), 1 //Tamaño del arreglo de SocketDescriptor. ); printf("Starting the server.\n"); // Necesitamos permitir al servidor aceptar conexiones entrantes de los clientes. peer->SetMaximumIncomingConnections(MAX_CLIENTS); REGISTER_STATIC_RPC(peer, PrintMessage); while (1) {

A continuación resta recibir los paquetes entrantes. packet=peer->Receive();

Luego de recibido el paquere, se verifica el contenido del primer byte de datos del paquete. El primer byte se usa para indicar el tipo de paquete que se ha recibido. while(packet) { switch (packet->data[0]) { case ID_REMOTE_DISCONNECTION_NOTIFICATION: printf("Otro cliente se ha desconectado.\n");

67

break; case ID_REMOTE_CONNECTION_LOST: printf("Otro cliente ha perdido la conexion.\n"); break; case ID_REMOTE_NEW_INCOMING_CONNECTION: printf("Otro cliente se ha conectado.\n"); break; case ID_CONNECTION_REQUEST_ACCEPTED: printf("Nuestra petición de conexión ”\ “ha sido aceptada.\n"); break; case ID_NEW_INCOMING_CONNECTION: printf("Hay una conexión entrante.\n"); break; case ID_NO_FREE_INCOMING_CONNECTIONS: printf("El servidor está lleno.\n"); break; case ID_DISCONNECTION_NOTIFICATION: printf("Un cliente se ha desconectado.\n"); break; case ID_CONNECTION_LOST: printf("Un cliente ha perdido la conexión.\n"); break; default: printf( "El mensaje con el identificacdor "\ "%i ha llegado.\n", packet->data[0] ); break; }

Liberar el paquete. peer->DeallocatePacket(packet); // Permanecer en el ciclo hasta que no haya mas paquetes. packet = peer->Receive(); } }

Liberar la conexión. RakNetworkFactory::DestroyRakPeerInterface(peer); return 0; }

68

2.4.2 CLIENTE (MAIN.CPP) Como se mencionó anteriormente, el cliente se conecta al servidor por medio del puerto 60000, inicialmente pide la dirección IP del servidor y una vez conectado espera a que el usuario ingrese una cadena para enviarla al servidor. Además de los archivos de cabecera del ejemplo anterior se incluirá string.h para poder utilizar strcpy(). //Cliente #include #include #include "RakNetworkFactory.h" #include "RakPeerInterface.h" #include "MessageIdentifiers.h" #define SERVER_PORT 60000 RakPeerInterface *peer;

Procedimiento para imprimir los mensajes recibidos. void PrintMessage(RPCParameters *rpcParameters) { printf("%s\n", rpcParameters->input); }

Se crea una cadena (str) para ingresar la dirección IP del servidor y los mensajes a envíar. int main(void) { char str[512]; Packet *packet; peer = RakNetworkFactory::GetRakPeerInterface(); peer->Startup(1,30,&SocketDescriptor(), 1); printf("Enter server IP or hit enter for 127.0.0.1\n"); gets(str); if (str[0]==0){ strcpy(str, "127.0.0.1"); } printf("Iniciando el Cliente.\n");

Aquí se realiza la conexión al servidor. peer->Connect( str, //Dirección SERVER_PORT, //Puerto remoto 0, //PasswordData 0 //PasswordDataLength

69

);

Se registra la función PrintMessage para poder llamarla remotamente. REGISTER_STATIC_RPC(peer, PrintMessage); while (1) {

Luego se detiene la ejecución del programa en espera de que el usuario ingrese una cadena para envíar. printf("Ingrese una cadena para mostrar en el servidor: "); gets(str);

Al envíar una cadena, se debe estar seguro que finaliza en NULL, por tanto, se debe definir la longitud a strlen(str)+1. RPC toma la longitud de bits de los datos (no de bytes), por lo que es necesario multiplicar el número de bytes por 8. //Verificar que no sea una cadena vacía. if (str[0]) peer->RPC( "PrintMessage", //Nombre de la función a llamar str, //Datos a envíar (strlen(str)+1)*8, //Longitud de datos HIGH_PRIORITY, //prioridad RELIABLE_ORDERED, //fiabilidad 0, //Orden de canales UNASSIGNED_SYSTEM_ADDRESS, //Dirección del destinatario true, // Broadcast 0, // TimeStamp UNASSIGNED_NETWORK_ID, //NetWorkID 0 //Respuesta desde el destino );

Recibir y gestionar los paquetes entrantes. packet=peer->Receive(); while(packet) { switch (packet->data[0]) { case ID_REMOTE_DISCONNECTION_NOTIFICATION: printf("Otro cliente se ha desconectado.\n"); break; case ID_REMOTE_CONNECTION_LOST: printf("Otro cliente ha perdido la conexión.\n"); break; case ID_REMOTE_NEW_INCOMING_CONNECTION: printf("Otro cliente se ha conectado.\n"); break; case ID_CONNECTION_REQUEST_ACCEPTED:

70

printf("Nuestra solicitud de conexión "\ "ha sido aceptada.\n"); break; case ID_NEW_INCOMING_CONNECTION: printf("Hay una conexión entrante.\n"); break; case ID_NO_FREE_INCOMING_CONNECTIONS: printf("El servidor está lleno.\n"); break; case ID_DISCONNECTION_NOTIFICATION: printf("Hemos sido desconectados.\n"); break; case ID_CONNECTION_LOST: printf("La conexión se ha perdido.\n"); break; default: printf("Ha llegado el mensaje con Id: %i.\n", packet->data[0]); break; } peer->DeallocatePacket(packet); // Permanecer en el ciclo hasta que no haya mas paquetes. packet = peer->Receive(); } }

Destruir la interfaz de comunicación. RakNetworkFactory::DestroyRakPeerInterface(peer); return 0; }

Sólo resta compilar ambos proyectos, ejecutar el servidor y una o varias instancias del cliente. 3.5 Irrlicht y RakNet, envío de estructuras En esta sección se mostrará una aplicación que envía estructuras de datos por medio de RakNet, a diferencia de la sección anterior en la que se enviaban solo cadenas de texto. Los archivos de proyecto se incluyen en el CD que acompaña a este documento. Dicha aplicación consiste en dos programas, un cliente y un servidor. El servidor contiene un cubo rotando y visible en pantalla y recibe eventos que le indican que se debe desplazar por el plano XZ o cambiar la dirección de su rotación; estos eventos son interceptados en el programa cliente y enviados como una estructura de datos al programa servidor. De nuevo es necesario hacer dos proyectos, un servidor y un cliente, en ambos proyectos se han incluído los archivos fuente de RakNet. El servidor incluye la librería 71

para acceder a la API de Irrlicht, se crea un nodo de escena (un cubo) y se le añade un animator de rotación. El programa cliente intercepta varias teclas: W, A, S, D y R que tienen la función de desplazar al cubo creado en el servidor en el plano XZ (teclas W, A, S y D) y cambiar la dirección de rotación (tecla R). Irrlicht.lib

Además de los dos archivos main.cpp (del cliente y servidor) se crea un tercer archivo llamado id_msg.h, el cual contiene una enumeración con los IDs de los mensajes que se envían entre el servidor y el cliente, además de la estructura de los mensajes y una función que convierte una serie de bytes (char *) en un objeto de la estructura de mensajes. 3.5.1 C ABECERA COMPARTIDA ( ID_MSG.CPP) Este archivo se debe incluír en ambos proyectos. Assert se incluye para abortar el programa en caso de error al convertir los datos del paquete a la estructura de mensaje. #include

La siguiente enumeración se usa para definir todos los IDs de los mensajes entre el cliente y el servidor, se debe iniciar siempre con ID_USER_PACKET_ENUM para que no se traslapen con los IDs de los mensajes propios de RakNet. //IDs de los eventos propios de juego enum { //Moviemiento de los jugadores ID_MOVE = ID_USER_PACKET_ENUM, //Rotación ID_ROTATE };

Ahora se define la estructura Message. En este caso sólo se necesita un ID para el tipo de mensaje a enviar y dos variables de coma flotante x, z para mover el cubo por el plano XZ. Las instrucciones #pragma pack(push,1) y #pragma pack(pop) son indispensables para poder enviar la estructura como una secuencia de bytes; lo que hacen es forzar al compilador a empaquetar la estructura como una sucesión de bytes alieados. #pragma pack(push, 1) struct Message{ unsigned char ID; float x, z; }; #pragma pack(pop)

72

Por último se define una función que permite convertir los datos de un paquete (char *) a un objeto de tipo Message. En caso de que la longitud del paquete no concuerde con el tamaño del mensaje se aborta la ejecución del programa. Message str2message(Packet *packet) { // Asignar los bytes de datos al tipo adecuado de estructura Message *m = (Message *) packet->data; assert(packet->length == sizeof(Message)); return *m; }

3.5.2 SERVIDOR (MAIN.CPP) Como en los ejemplos anteriores, se incluyen los archivos de cabecera de Irrlicht y de RakNet, stdio para entrada y salida estándar y el archivo id_msg.h que está fuera de la carpeta del poyecto (anteponer ../). También se vinclulará con Irrlicht.DLL, se oculta la consola y se indica que se utilizará el nombre de espacio irr. #include #include #include "RakNetworkFactory.h" #include "RakPeerInterface.h" #include "MessageIdentifiers.h" #include "../id_msg.h" //Vincular ocn Irrlicht.dll #pragma comment(lib, "Irrlicht.lib") //Ocultar la ventana de consola #pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup") using namespace irr;

Luego se define el número máximo permitido de conexiones al servidor y el puerto a utilizar. #define MAX_CLIENTS 10 #define SERVER_PORT 60000

Se declara la interfaz de comunicación peer. RakPeerInterface *peer;

Ahora se inicia el dispositivo Irrlicht, se establece el título para la ventana y se crean los punteros a manejador de video y de escena. int main(void) {

73

/* --- Objetos de Irrlicht */ IrrlichtDevice *device = irr::createDevice( video::EDT_DIRECT3D9, //Manejador de video core::dimension2d(640,480), //Tamaño de la ventana 16, //Profundidad de color false, //Pantalla completa false, //stencil buffer false, //vsync 0 //Manejador de eventos ); device->setWindowCaption(L"Ejemplo de Irrlicht y RakNet por Cristiandlr"); //Punteros video::IVideoDriver *driver = device->getVideoDriver(); scene::ISceneManager *smgr = device->getSceneManager();

Luego se crea un cubo, se le asigna una textura y se desactiva la iluminación a falta de una luz dinámica en la escena. También se le agrega un animator para hacerlo rotar respecto del eje Y. //Agregamos un nodo de escena (un cubo) para poder mover scene::ISceneNode *node = smgr->addCubeSceneNode(); //moverlo al origen node->setPosition(core::vector3df(0,0,0)); //agregar una textura al cubo node->setMaterialTexture(0,driver->getTexture("./cruz.jpg")); //desactivar la iluminación node->setMaterialFlag(video::EMF_LIGHTING, false); //Agregar un animator de rotación scene::ISceneNodeAnimator *a=0; a = smgr->createRotationAnimator(core::vector3df(0,2.0f,0)); node->addAnimator(a); a->drop();

Para poder visualizar la escena se necesita añadir una cámara, la cual se apuntará al origen que es donde se colocó inicialmente el cubo y se posiciona en (0,0,-50). //añadir una cámara y apuntarla al origen smgr->addCameraSceneNode( 0, //parent core::vector3df(0,0,-50), //posicion core::vector3df(0,0,0) //ver hacia );

74

Ahora se definen los objetos de RakNet, un puntero packet para recibir los paquetes que lleguen desde los clientes, se inicia la interfaz de comunicación y se establece el número máximo de conexiones entrantes. /* --- Objetos de RakNet */ Packet *packet; peer = RakNetworkFactory::GetRakPeerInterface(); peer->Startup(MAX_CLIENTS, 30, &SocketDescriptor(SERVER_PORT,0), 1); printf("Starting the server.\n"); // We need to let the server accept incoming connections from the clients peer->SetMaximumIncomingConnections(MAX_CLIENTS);

Se requiere un objeto de tipo Message para “reacomodar los bytes” que llegan en los paquetes a su estructura original. La variable booleana direction se utiliza para determinar si el animator de rotación gira a la derecha o a la izquierda. /* --- Objetos propios del ejemplo */ Message m; bool direction=true;

Se inicia el ciclo principal, se repite mientra el dispositivo esté en ejecución. while (device->run()) { driver->beginScene( true, //backbuffer true, //zbuffer video::SColor(255,100,101,140) //color );

Se captura el paquete. Si el ID del paquete (packet->data[0]) es mayor o igual a se sabrá que es un mensaje definido para este programa y no uno de RakNet, entonces se asigna al objeto m para acceder a la información que contiene. ID_USER_PACKET_ENUM

packet=peer->Receive(); while(packet) { if (packet->data[0]>= ID_USER_PACKET_ENUM) m = str2message(packet); switch (packet->data[0]) {

Se compara el ID del paquete para verificar el tipo de mensaje. 75

case ID_REMOTE_DISCONNECTION_NOTIFICATION: printf("Otro cliente se ha desconectado.\n"); break; case ID_REMOTE_CONNECTION_LOST: printf("Otro cliente ha perdido la conexión.\n"); break; case ID_REMOTE_NEW_INCOMING_CONNECTION: printf("Otro cliente se ha conectado.\n"); break; case ID_CONNECTION_REQUEST_ACCEPTED: printf("Nuestra conexión ha sido aceptada.\n"); break; case ID_NEW_INCOMING_CONNECTION: printf("Hay una conexión entrante.\n"); break; case ID_NO_FREE_INCOMING_CONNECTIONS: printf("El Servidor está lleno.\n"); break; case ID_DISCONNECTION_NOTIFICATION: printf("Un cliente se ha desconectado.\n"); break; case ID_CONNECTION_LOST: printf("Un cliente perdió la conexión.\n"); break;

En caso de que el ID corresponda con ID_MOVE, se establece la nueva posición del cubo en la escena. case ID_MOVE: node->setPosition(core::vector3df(m.x, 0, m.z)); break;

Si el ID corresponde con ID_ROTATE, se cambia la dirección de rotación del cubo. Como este nodo ya contiene un animator de rotación, debe ser eliminado antes de agregar el nuevo animator con la dirección adecuada. case ID_ROTATE: node->setPosition(core::vector3df(m.x, 0, m.z)); direction=!direction; //quitar el animator actual node->removeAnimators(); if(direction) { a = smgr->createRotationAnimator( core::vector3df(0,2.0f,0) ); } else { a = smgr->createRotationAnimator( core::vector3df(0,-2.0f,0) ); }

76

node->addAnimator(a); a->drop(); break;

Si el ID del mensaje no está entre las opciones, simplemente se depliega un mensaje al usuario por medio de la consola. Luego de liberar el paquete, se verifica si hay más paquetes en espera, si los hay, continúa el el ciclo. default: printf("Mensaje desconocido, id=%i.\n", packet->data[0]); break; } //Liberar el paquete. peer->DeallocatePacket(packet); // Permanecer en el ciclo hasta que no haya más paquetes. packet = peer->Receive(); }

Sólo resta dibujar la escena. smgr->drawAll(); driver->endScene(); }

Se libera la interfaz de comunicación antes de salir por completo. RakNetworkFactory::DestroyRakPeerInterface(peer); return 0; }

3.5.3 CLIENTE (MAIN.CPP) Para iniciar, se incluyen los archivos de cabecera para entrada y salida estándar en la consola, string.h para manejo de cadenas, cabeceras de RakNet y por último id_msg.h. //Cliente #include #include #include #include "RakNetworkFactory.h" #include "RakPeerInterface.h" #include "MessageIdentifiers.h" #include "../id_msg.h"

Ahora se define el puerto y la interfaz de comunicación, en este programa no se incluye la librería Irrlicht porque los eventos se van a interceptar por medio de kbhit().

77

#define SERVER_PORT 60000 RakPeerInterface *peer; int main(void) { char str[512]; Packet *packet; peer = RakNetworkFactory::GetRakPeerInterface(); peer->Startup(1,30,&SocketDescriptor(), 1);

Se solicita al usuario que indique la dirección IP del Servidor o que presione ENTER para 127.0.0.1. printf("Ingrese la dirección IP del Servidor o ENTER para 127.0.0.1\n"); gets(str); if (str[0]==0){ strcpy(str, "127.0.0.1"); } printf("Iniciando el Cliente.\n"); peer->Connect(str, SERVER_PORT, 0,0);

La variable key va a guardar la tecla presionada por el usuario, se crea también un objeto de tipo Message para envíar los datos al servidor y dos variables que guardan la posición del cubo en el servidor en el plano XZ. unsigned char key=0; Message m; float X=0, Z=0;

La variable conectado indica si el Servidor ya ha aceptado o no la solicitud de conexión. Una vez acepatada la conexión se guarda la dirección del servidor en el objeto servidor y se interceptan los eventos de teclado. bool conectado = false; SystemAddress servidor; while (1) { if(kbhit() && conectado) { key = getch(); if(key==27) { //salir del programa break; }else if(key=='w') { //Hacia adelante m.ID = ID_MOVE; Z+=5; } else if (key=='s'){ //Hacia atrás m.ID = ID_MOVE;

78

Z-=5; } else if (key=='d'){ //Hacia la derecha m.ID = ID_MOVE; X+=5; } else if (key=='a'){ //Hacia la izquierda m.ID = ID_MOVE; X-=5; } else if (key=='r'){ //Cambiar la dirección de rotación m.ID = ID_ROTATE; printf("Cambiar rotación.\n"); }

Una vez definidos los valores de X, Z y m.ID se envía la estructura al servidor. Para envíar cualquier estructura basta con una conversión forzada a char * para manejarla como una secuencia de bytes: (char *)&m; m.x = X; m.z = Z; peer->Send( (char *)&m, sizeof(m), HIGH_PRIORITY, // PacketPriority RELIABLE_ORDERED, // PacketReliability 0, // orderingChannel servidor, // systemAddress false // broadcast (true) );

Imprimir la nueva posición del cubo. printf("Desplazar: X=%.2f, Z=%.2f\n", X, Z); }

Se reciben los paquetes desde el servidor (si los hay). packet=peer->Receive(); while(packet) { switch (packet->data[0]) { case ID_REMOTE_DISCONNECTION_NOTIFICATION: printf("Otro cliente se ha desconectado.\n"); break; case ID_REMOTE_CONNECTION_LOST: printf("Otro cliente ha perdido la conexión.\n"); break; case ID_REMOTE_NEW_INCOMING_CONNECTION: printf("Otro cliente se ha conectado.\n"); break; case ID_CONNECTION_REQUEST_ACCEPTED: printf("Nuestra conexión ha sido aceptada.\n"); conectado = true;

79

servidor = packet->systemAddress; break; case ID_NO_FREE_INCOMING_CONNECTIONS: printf("El Servidor está lleno.\n"); break; case ID_DISCONNECTION_NOTIFICATION: printf("Hemos sido desconectados.\n"); break; case ID_CONNECTION_LOST: printf("Se perdió la conexión.\n"); break; default: printf("Nuevo mensaje con id: %i.\n", packet->data[0]); break; } peer->DeallocatePacket(packet); // Permanecemos en el ciclo hasta que no haya más paquetes. packet = peer->Receive(); } }

Por último se libera la interfaz de comunicación. La figura 3.4 muesta la aplicación en ejecución. RakNetworkFactory::DestroyRakPeerInterface(peer); return 0; }

Figura 3.4. Una muestra de la aplicación en ejecución. Atrás se puede ver la ventana del Servidor con el cubo en rotación. La consola muestra los desplazamientos que han sido enviados al servidor.

80

Capítulo IV

Aplicación: irrArena

IrrArena es un juego de primera persona que utiliza Irrlicht y RakNet. Se pueden cargar diversos escenarios de Quake 3 y también diferentes personajes utilizando modelos MD2. También incorpora irrKlang para manejo de audio 2D y 3D. Esta figura muestra el logotipo del juego.

81

82

4.1 Introducción Este capítulo presenta el diseño completo del juego de video de primera persona, en red y utilizando Irrlicht y RakNet. irrArena es un pequeño juego de acción de primera persona inspirado en el juego Quake 3 Arena de Id Software. El objetivo del juego es moverse a través de todo el escenario o campo de batalla eliminando a los demás jugadores. Cada jugador controla a su personaje a través de una computadora conectada al Servidor Arena, el cual refleja las acciones de cada jugador a las otras computadoras que ejecutan el programa Cliente en la red. Cuando el nivel de vida de un jugador llega a cero éste es reubicado en el escenario (conservando sus municiones) y el jugador que le ha eliminado suma un punto en el conteo de eliminados. El juego termina cuando uno de los jugadores alcanza el límite de enemigos eliminados especificado en la configuración del servidor. Tanto el Cliente como el Servidor son configurables por medio de los archivos config.xml que se encuentran en el mismo directorio del ejecutable. En el caso del servidor pueden configurarse diferentes opciones, tales como: el número de enemigos eliminados para ganar la partida, el número máximo permitido de clientes, el puerto a utilizar y también se deben cargar los diferentes mapas de Quake 3 (archivos pk3). Para el cliente las opciones de configuración son: Dirección IP del Servidor, puerto, volumen de los efectos de sonido, configuración de video, lista de reproducción de audio (pueden agregarse diferentes archivos de mp3 para escuchar mientras juega), los modelos de los jugadores (md2) y el listado de mapas de Quake 3.

4.2 Características del Juego • • • •



irrArena es un juego de acción de primera persona. Multijugador: El juego solo funciona en red y con dos o más jugadores. Gráficos 3D: Utiliza el Engine Irrlicht para todo lo relacionado con el manejo de gráficos 3D, cámaras, escenarios, modelos, etc. Audio 2D y 3D: Utilizando el motor de audio irrKlang se reproducen diferentes sonidos en 2D (tal como música de fondo) y 3D (por ejemplo, los pasos de otro jugador que se acerca hacia (o que se aleja de) el oyente o jugador). Configurable: El juego es configurable por medio de archivos XML. Se pueden cambiar ciertos valores como la gravedad y velocidad de salto del jugador para cada mapa. También se pueden añadir nuevos escenarios y modelos al juego, elegir el manejador de video (Direct3D, OpenGL, Software…), modo de pantalla completa o ventana, etc.

83

4.3 Instalación y puesta en marcha del juego En el CD que acompaña a este texto se incluye tanto el código fuente de la aplicación como los binarios. En esta sección se describe la configuración del juego e instalación de los binarios del juego. En el directorio Capítulo IV/bin hay dos directorios más, cliente y servidor. Se debe copiar la carpeta "Servidor" a la computadora de la red que se desea utilizar para servidor y la carpeta "Cliente" a cada computadora donde se va a ejecutar un cliente o instancia del juego. Se ejecuta "ServidorArena.exe" en el servidor, se selecciona un mapa (utilizando el número que lo identifica) y en cada cliente se ejecuta "arena.exe" y se selecciona un jugador (también por medio del número que lo identifica). 4.3.1 TECLAS UTILIZADAS PARA JUGAR Las teclas utilizadas para jugar son las siguientes: Movimiento: W - Caminar hacia adelante S - Caminar hacia atrás A - Caminar hacia la izquierda D - Caminar hacia la derecha BARRA ESPACIADORA - Saltar Información: I - Información de municiones y vida del jugador L - Listado de jugadores conectados a la partida Reproducción de audio: NUM * - Play / Pause NUM / - Siguiente canción NUM - - Bajar volumen NUM + - Subir volumen Acción: CTRL IZQ - Risa BACKSPACE - Suicidio Clic izquierdo - Disparar Salida: Para salir del juego se utiliza la tecla ESC. 84

A continuación se tratará acerca de la configuración del cliente y del servidor. El juego ya viene preconfigurado por lo que no necesita crear estos archivos manualmente. Si Ud. quiere modificarlos, por ejemplo para jugar en pantalla completa, entonces revise ésta documentación. 4.3.2 CONFIGURACIÓN DEL S ERVIDOR ( CONFIG.XML) Ahora se analizará parte por parte la estructura del archivo de configuración del Servidor irrArena: config.xml.

Toda la configuración del juego se encuentra dentro de las etiquetas y

Se define el número de bajas que un jugador debe efectuar sobre los demás para ganar la partida. También el número máximo de conexiones y el puerto a utilizar.

Solo resta agregar el listado de mapas y los puntos válidos de cada mapa. Estos puntos se utilizan en el servidor para ubicar / reubicar a los jugadores en el mapa de Quake 3. También se utilizan para posicionar las municiones y energía que los jugadores pueden recoger. Es importante notar que el id del mapa debe coincidir con el id definido en el archivo de configuración del cliente, donde se definen otros valores como skyboxes29, gravedad, etc.

29

Un skybox es un cubo de dimensiones muy grandes que se ubica alrededor del escenario y que contiene seis texturas (una por cada lado) que simula el cielo en la escena.

85



4.3.3 CONFIGURACIÓN DEL CLIENTE ( CONFIG.XML) Ahora se examinará la configuración del cliente. Al igual que el anterior, se coloca toda la configuración entre las etiquetas y

86

Lo primero es definir la dirección IP del servidor y el puerto a utilizar.

Ahora se establece el volumen de los efectos de sonido del juego, tal como el sonido de los pasos de los jugadores, los disparos, la risa del jugador, etc. El volumen puede variar de 0.00 a 1.00.

• • • • •

En la configuración de video los parámetros son: completa: 1=pantalla completa, 0=modo de ventana anchura: Anchura en pixeles de la ventana (o modo de video si se activa pantalla completa) altura: Altura en pixeles de la ventana (o modo de video si se activa pantalla completa) profundidad: Bits de color; cantidad de colores = pow(2, profundidad) driver: Manejador de video 1=Direct3D 9 2=Direct3D 8.1 3=Open GL 1.5 4=Software Renderer



También es posible configurar una lista de reproducción de audio. Esta lista se ejecutará mientras el juego esté en ejecución y es independiente en cada cliente. Los parámetros principales son: activa: 1=Inicialmente activa y 0=Inicialmente pausada. volumen: Volumen de las pistas de audio. Toma valores de 0.00 a 1.00 aleatorio: 1=Reproducción aleatoria, 0=Ejecutar pistas ordenadas por su id.

Las etiquetas ítem_audio permiten añadir pistas de audio, solo basta asignar un id numérico y la ubicación física del archivo de audio. Los formatos que se aceptan son wav, ogg, mp3, mod, xm, it, s3m y demás formatos aceptados por irrKlang.

87



La configuración de jugadores permite cargar modelos MD2 al juego. Las etiquetas se colocan dentro de las etiquetas y . Se debe definir el archivo de textura, la escala del modelo, los sonidos, etc. Los parámetros de la etiqueta son: • id: int. Un entero que identifica al modelo • nombre: str. Descripcion del modelo • archivo: str. Modelo MD2 • textura: str. Textura del modelo • escala: float. Escala del modelo • quitar_y: float. Diferencia entre el punto de rotación del modelo y los pies del mismo • snd_saltar: str. Archivo de audio a ejecutar cuando el jugador salta • snd_morir: str. Archivo de audio a ejecutar cuando el jugador sea eliminado • snd_reir: str. Archivo de audio a ejecutar para que el jugador exprese risa



88

Por último se configuran los archivos de los mapas o niveles de Quake 3. Como ya se mencionó, el id debe coincidir con el id del archivo de configuración del servidor. Los atributos de cada etiqueta son los siguientes: • nombre: (str) descripcion del mapa • autor: (str) autor del mapa • archivo: (str) pk3 • bsp: (str) nombre del archivo bsp contenido en el pk3 • cielo: (str) nombre de los archivos para crear un skybox (sin incluir _ft, _bk, _up, _lf...) • cielo_ext: (str) formato de las imagenes de skybox • velocidad_salto: (float) velocidad inicial de salto del jugador Todas las etiquetas deben ir dentro de y .

Get in touch

Social

© Copyright 2013 - 2024 MYDOKUMENT.COM - All rights reserved.