g g g gg g *udgr hq ,qjhqlhutd ,qirupiwlfdrua.ua.es/dspace/bitstream/10045/93558/1/game... · g g g...
TRANSCRIPT
Grado en Ingeniería Informática
Trabajo Fin de Grado
Autor: Daniel Ponsoda Montiel
Tutor/es: Francisco José Gallego Durán
Mayo 2019
U N I V E R S I D A D D E A L I C A N T E
ESCUELA POL ITÉCNICA SUPERIOR Departamento de Ciencia de la Computación e Inteligencia Artificial
GRADO EN INGENIERÍA INFORMÁTICA Trabajo Fin de Grado
Daniel Ponsoda Montiel [email protected]
TUTORES
Francisco José Gallego Durán
27 de mayo de 2019
Acerca de las imágenes, figuras y logos: Las capturas de los motores Unity3D, Unreal Engine y CryEngine están extraídas de la documentación
oficial y son propiedad de sus respectivos autores. La captura del juego “Livingstone, Supongo” es propiedad de Opera Soft. Los logos mostrados en el texto representan marcas de sus respectivos
propietarios. El resto de imágenes y figuras son creación del autor del TFG.
A Manuela, por su inestimable compañía, e interminable paciencia y apoyo.
AGRADECIMIENTOS
Quisiera agradecer en primer lugar a todos aquellos profesores que me han guiado tan
acertadamente en este viaje hasta el último curso de la carrera y que, por su dedicación,
vocación, y gusto por su trabajo hacen grande a la Universidad de Alicante. En especial a
Domingo Gallardo López, David Gil Méndez, Virgilio Gilart Iglesias, Eva Gómez Ballester,
Antonio Jimeno Morenilla, Javier Montoyo Bojo, Jerónimo Mora Pascual, Francisco Moreno
Seco, Cristian Neipp López, José Oncina Carratalá, Jesús Peral Cortés, Carlos Pérez Sancho,
Pedro Ponce de León, Julio Rosa Herranz, Felipe Sánchez Martínez y David Tomás Diaz.
Además, quiero agradecer especialmente por su gran labor docente y por dar a cada una
de sus clases un enfoque tan inspirador que se han convertido en un referente para mí a nivel
profesional y personal, al profesor Sergio Ramón Ferry y a mi tutor Francisco Gallego Durán.
También doy mi más sincero agradecimiento a mis grandes amigos Aarón Adrover López
y Manuel Aniorte Sánchez por su amistad y por transmitirme su fuerza para emprender
proyectos igual de grandes.
Por supuesto, también doy las gracias a toda mi familia. En especial a mis padres y
hermanos por darme un inmejorable ejemplo a seguir y por estar a mi lado ofreciéndome su
incondicional apoyo y afecto, sin el cual no estaría hoy escribiendo estas líneas.
También, por su apoyo diario e inestimable amistad, quiero dar un agradecimiento muy
especial a mis compañeros y amigos Juan Miguel Castillo Zaragoza y Luis Pérez Pérez, que
en estos últimos cuatro años han sido y serán siempre como hermanos para mí.
Y, por último, y no menos importante, a mi señora Manuela Ballester Levia, a quien dedico
mi trabajo y la cual es, sin ninguna duda, la mejor confidente, compañera y amiga que
cualquier persona pudiera soñar tener a su lado. Gracias por todo lo que me das y lo poco que
pides a cambio. Te quiero.
INDICE DE CONTENIDO
Agradecimientos ............................................................................................................. vii
Índice de figuras...............................................................................................................xv
Preámbulo ......................................................................................................................xix
Origen y propósito de los game engines ......................................................................xix
Acerca de la industria de los videojuegos ..................................................................xxiii
1 Introducción ............................................................................................................ 27
1.1 Objetivos ............................................................................................................. 27
1.2 Motivación .......................................................................................................... 27
1.3 Solución inicial .................................................................................................... 29
1.4 Problema en plataformas móviles ....................................................................... 30
1.5 Solución para plataformas móviles ...................................................................... 31
1.6 Alcance del proyecto ........................................................................................... 32
2 Estudio de game engines existentes ........................................................................ 35
2.1 Unity3D .............................................................................................................. 35
2.1.1 El editor ....................................................................................................... 36
2.1.2 El sistema de scripting ................................................................................. 38
2.1.3 Ventajas e inconvenientes ............................................................................ 39
2.2 Unreal Engine ..................................................................................................... 40
2.2.1 El editor ....................................................................................................... 41
2.2.2 El sistema de scripting ................................................................................ 43
2.2.3 Ventajas e inconvenientes ............................................................................ 44
2.3 CryEngine ........................................................................................................... 45
2.3.1 El editor ...................................................................................................... 45
2.3.2 El sistema de scripting ................................................................................ 47
2.3.3 Ventajas e inconvenientes ............................................................................ 47
2.4 Mejoras que aportará nuestro motor ................................................................... 48
3 Modelo de desarrollo y planificación ....................................................................... 49
3.1 Modelo iterativo .................................................................................................. 49
3.2 Planificación adaptativa ..................................................................................... 50
3.2.1 Plan general ................................................................................................. 51
3.2.2 Plan detallado ............................................................................................. 51
4 Definición de la arquitectura .................................................................................. 53
4.1 Patrón Entidad-Componente-Sistema ................................................................. 53
4.2 Despliegue ........................................................................................................... 55
4.3 Arquitectura del reproductor .............................................................................. 56
4.4 Formato y recursos del proyecto de juego .......................................................... 58
5 Diseño del interfaz .................................................................................................. 59
5.1 Interfaz gráfica del editor .................................................................................... 59
5.2 Interfaz de consola .............................................................................................. 62
6 Elección de las dependencias .................................................................................. 65
6.1 Librería de matemáticas ..................................................................................... 65
6.2 Librería de gráficos ............................................................................................. 66
6.2.1 Ogre ............................................................................................................. 66
6.2.2 Irrlicht ......................................................................................................... 67
6.2.3 OpenSceneGraph ......................................................................................... 68
6.2.4 Decisión sobre la librería de gráficos ............................................................ 69
6.3 Librería de física .................................................................................................. 69
6.4 Lenguaje de scripting .......................................................................................... 71
6.4.1 Lua .............................................................................................................. 71
6.4.2 JavaScript .................................................................................................... 72
6.4.3 Python ......................................................................................................... 73
6.4.4 Decisión sobre el lenguaje de scripting ......................................................... 73
6.5 Interfaz gráfica de usuario para el editor ............................................................ 74
6.5.1 Qt ................................................................................................................ 74
6.5.2 wxWidgets ................................................................................................... 75
6.5.3 Decisión sobre la librería de GUI ................................................................. 75
6.6 Librería de sockets .............................................................................................. 76
7 Configuración del entorno ....................................................................................... 77
7.1 Preparar OpenSceneGraph .................................................................................. 80
7.1.1 Compilar para Windows .............................................................................. 80
7.1.2 Compilar para Linux ................................................................................... 81
7.1.3 Compilar para Android ................................................................................ 82
7.2 Preparar Bullet Physics ...................................................................................... 83
7.2.1 Compilar para Windows .............................................................................. 83
7.2.2 Compilar para Linux ................................................................................... 85
7.2.3 Compilar para Android ................................................................................ 85
7.3 Preparar Lua ....................................................................................................... 86
7.3.1 Compilar para Windows .............................................................................. 86
7.3.2 Compilar para Linux ................................................................................... 87
7.3.3 Compilar para Android ............................................................................... 87
7.4 Preparar el framework Qt ................................................................................... 88
7.5 Preparar Asio ...................................................................................................... 88
7.6 Proyecto del reproductor para Windows/Linux.................................................. 89
7.7 Proyecto del reproductor para Android .............................................................. 90
7.8 Proyecto de la consola para Windows/Linux ..................................................... 90
7.9 Proyecto para el editor gráfico ............................................................................ 91
7.9.1 Integración de Qt con OpenSceneGraph (osgQt) ........................................ 92
8 Diseño deL código .................................................................................................. 95
8.1 Sistema de administración de assets ................................................................... 95
8.2 Sistema de configuración ..................................................................................... 96
8.3 Sistema de consola cliente / servidor .................................................................. 97
8.4 Entidad-Componente-Sistema ............................................................................. 98
8.5 Sistema de gráficos ............................................................................................. 99
8.6 Sistema de entrada ........................................................................................... 100
8.7 Sistema de log ................................................................................................... 101
8.8 Sistema de física ............................................................................................... 102
8.9 Sistema de scripting .......................................................................................... 103
8.10 Sistema de transformación ................................................................................ 104
9 Implementación .................................................................................................... 105
9.1 Sistema de log ................................................................................................... 105
9.1.1 Necesidad de derivar la salida de log a múltiples destinos ........................ 105
9.1.2 Log como stream con información de fichero y línea ................................. 106
9.1.3 Desactivación de logs de depuración en modo release ................................ 107
9.2 Administración de recursos de disco ................................................................. 109
9.3 Dependencias en el sistema ECS ....................................................................... 112
9.4 Interoperabilidad entre librerías de gráficos y física .......................................... 113
9.5 Resolución de riesgos de programación concurrente .......................................... 114
9.5.1 Sistema de input de usuario ....................................................................... 115
9.5.2 Ejecución de script desde consola remota .................................................. 115
9.6 Sincronización de relojes ................................................................................... 116
9.7 Abstracción mediante fachadas ......................................................................... 117
9.8 Acerca del editor gráfico ................................................................................... 119
10 Ejemplos de uso .................................................................................................... 121
10.1 Primer proyecto ................................................................................................ 121
10.2 Uso de la consola ............................................................................................... 122
10.3 Demostración acelerómetro: “DemoBall.lua” .................................................... 124
10.4 Demostración rendimiento: “DemoRandom.lua” ............................................... 126
10.5 Demostración minijuego: “DemoMarble.lua” .................................................... 127
11 Pruebas de rendimiento ........................................................................................ 129
12 Posibles mejoras .................................................................................................... 133
12.1 Mejoras en el sistema de gráficos ...................................................................... 133
12.2 Compilación del script....................................................................................... 133
12.3 Prescindir de algunas dependencias................................................................... 134
12.4 Cifrado del código y assets ................................................................................ 135
13 Conclusiones ......................................................................................................... 137
14 Referencias ........................................................................................................... 139
15 Glosario ................................................................................................................ 141
Anexo I: Librería de matemáticas ................................................................................. 145
Vectores ..................................................................................................................... 145
Puntos........................................................................................................................ 150
Matrices ..................................................................................................................... 151
Cuaterniones .............................................................................................................. 159
Anexo II. Sistema de gráficos ........................................................................................ 163
Pipeline de procesamiento de gráficos ........................................................................ 163
La cámara .................................................................................................................. 165
Matriz Modelo-Vista-Proyección ................................................................................ 166
Anexo III: Manual del API ............................................................................................ 169
AssetMapper .............................................................................................................. 169
CameraComponent .................................................................................................... 171
DrawableComponent ................................................................................................. 173
Entity ........................................................................................................................ 174
ion (alias de SystemManager) .................................................................................... 176
Material ..................................................................................................................... 178
PhysicsComponent ..................................................................................................... 180
Random ..................................................................................................................... 182
Shapes ........................................................................................................................ 183
TransformComponent ................................................................................................ 184
ÍNDICE DE FIGURAS
Figura 0.1. Livingstone Supongo, 1986, creado por el estudio español Opera Soft ......... xx
Figura 0.2. Juego Crysis 3. Realizado por Crytek con su propio motor CryEngine ...... xxii
Figura 0.3. Beneficios interanuales de videojuegos según Newzoo ............................... xxiv
Figura 0.4. Crecimiento del mercado de videojuegos según Newzoo ............................ xxiv
Figura 0.5. Salario de desarrolladores de videojuegos según Texterity ......................... xxv
Figura 1.1. Proceso general de ajuste y pruebas .............................................................. 28
Figura 1.2. Boceto inicial de la arquitectura del motor ................................................... 29
Figura 1.3. Esquema de trabajo editor/reproductor ........................................................ 31
Figura 2.1. Interfaz del editor de juegos de Unity3D ....................................................... 36
Figura 2.2. Ventana Inspector de Unity3D ...................................................................... 37
Figura 2.3. Comparativa de editores entre Unity y Unreal ............................................. 42
Figura 2.4. Sistema de Blueprints de Unreal Engine ....................................................... 43
Figura 2.5. Editor de CryEngine ..................................................................................... 46
Figura 2.6. Interfaz de edición de objetos de CryEngine ................................................. 46
Figura 3.1. Tareas de una iteración ................................................................................. 49
Figura 4.1. Ejemplo Entidad-Componente-Sistema ......................................................... 54
Figura 4.2. Despliegue del editor y los reproductores ...................................................... 56
Figura 4.3. Arquitectura del player ................................................................................. 57
Figura 5.1. Mockup pantalla principal del editor ............................................................ 59
Figura 6.1. Logo de Ogre ................................................................................................. 66
Figura 6.2. Logo de Irrlicht ............................................................................................. 67
Figura 6.3. Logo de OpenSceneGraph ............................................................................. 68
Figura 6.4. Logo de Bullet ............................................................................................... 69
Figura 6.5. Logo de Lua .................................................................................................. 71
Figura 6.6. Logo de JavaScript ........................................................................................ 72
Figura 6.7. Logo de Python ............................................................................................. 73
Figura 6.8. Logo de Qt .................................................................................................... 74
Figura 6.9. Logo de wxWidgets ....................................................................................... 75
Figura 8.1. Diagrama de clases. Sistema de administración de assets ............................. 95
Figura 8.2. Diagrama de clases. Sistema de configuración ............................................... 96
Figura 8.3. Diagrama de clases. Sistema de consola cliente / servidor ............................ 97
Figura 8.4. Diagrama de clases. Entidad-Componente-Sistema ....................................... 98
Figura 8.5. Diagrama de clases. Sistema de gráficos ........................................................ 99
Figura 8.6. Diagrama de clases. Sistema de entrada ..................................................... 100
Figura 8.7. Diagrama de clases. Sistema de log ............................................................. 101
Figura 8.8. Diagrama de clases. Sistema de física.......................................................... 102
Figura 8.9. Diagrama de clases. Sistema de scripting .................................................... 103
Figura 8.10. Diagrama de clases. Sistema de transformación ........................................ 104
Figura 9.1. Diagrama de dependencias entre componentes ........................................... 113
Figura 9.2. Sistemas de coordenadas en Bullet y OSG .................................................. 113
Figura 10.1. Captura del primer proyecto de ejemplo ................................................... 122
Figura 10.2. Demostración acelerómetro ....................................................................... 124
Figura 10.3. Demostración física .................................................................................... 126
Figura 10.4. Juego de demostración ............................................................................... 128
Figura 11.1 DemoRandom.lua ....................................................................................... 129
Figura 11.2. Gráfica de rendimiento en la versión PC ................................................... 130
Figura 11.3. Gráfica de rendimiento en la versión Android ........................................... 131
Figura 0.1. Vectores ....................................................................................................... 146
Figura 0.2. Magnitud de vector ..................................................................................... 147
Figura 0.3. Vector normalizado ..................................................................................... 148
Figura 0.4. Regla de mano derecha ................................................................................ 149
Figura 0.5. Multiplicación de matrices ........................................................................... 153
Figura 0.6. Ejes de rotación ........................................................................................... 157
Figura 0.7. Demostración del efecto gimbal lock ........................................................... 160
Figura 0.1. Matriz cámara ............................................................................................. 165
Figura 0.2. Transformación con la matriz modelo ......................................................... 167
Figura 0.3. Proyección de un punto sobre un plano ...................................................... 168
Daniel Ponsoda Montiel
xix
PREÁMBULO
Por simplicidad, podemos definir el motor de un coche como un conjunto de partes, cada
una con distinta finalidad, que colaboran para realizar una tarea común que en este caso sería
mover el vehículo.
Análogamente, un motor de videojuegos o game engine, está construido de la misma forma
(y de ahí su nombre), solo que no se trata de piezas físicas sino de componentes software, y
su finalidad principal es la de reproducir juegos en un ordenador y servir de ayuda en su
proceso de desarrollo.
Un game engine es un sistema compuesto a su vez de varios subsistemas como el de
gráficos, física, colisiones, sonido, matemáticas, inteligencia artificial, scripting, red, interfaz
de usuario, etc. por tanto se presenta como un objeto de estudio muy interesante que
implementa y unifica multitud de disciplinas de varios campos científicos y técnicos. Pero para
conocer qué elementos lo forman exactamente y por qué están ahí, debemos saber primero de
dónde surge el desarrollo de un game engine.
Origen y propósito de los game engines En la historia reciente, ha habido sin duda una explosión en la evolución de la tecnología.
Hace no demasiados años, el hecho de que una persona de a pie pudiera tener un ordenador
en casa era algo que sólo podíamos ver en películas de ciencia ficción. Se consideraba un
producto caro, voluminoso y delicado, dirigido sólo a científicos o expertos en tecnología.
La línea que separaba la realidad de la ficción empezó a disiparse a finales del siglo pasado,
hasta tal punto que, en la actualidad, la mayoría de personas que vivimos en países avanzados
tenemos en nuestro bolsillo un ordenador miles de veces más potente que aquel que llevó al
Game engine multiplataforma
xx
hombre a la luna hace 50 años, y tan sencillo de manejar que cualquier niño puede usarlo.
Esto ha permitido la introducción de todo tipo aplicaciones y oportunidades de mercado antes
impensables como son los videojuegos de última generación, capaces de generar en tiempo real
imágenes que rozan el fotorrealismo.
Volviendo atrás, en la gloriosa época de los
primeros ordenadores personales, si conocíamos bien la
máquina, producir un videojuego era en realidad una
tarea relativamente sencilla. De hecho, una sola
persona con habilidad en programación y paciencia
dibujando sprites pixel a pixel podía diseñar y realizar
un juego comercial en cuestión de unos pocos meses (o
incluso semanas).
No obstante, como he mencionado, un aspecto fundamental era conocer bien la máquina.
Y es que, la falta de precedentes, documentación y herramientas de desarrollo era una fuente
de problemas para estos programadores pioneros que, motivados por este nuevo mundo que
les abría la tecnología, sin darse cuenta adquirían competencias transversales del campo de la
programación muy demandadas en la actualidad como la capacidad de desarrollo utilizando
nuevas tecnologías con escasa información disponible y habilidades para exprimir al máximo
las limitaciones técnicas del sistema con el que trabajan.
Por otro lado, el éxito del desarrollador que creaba en solitario un videojuego se debía en
parte a que las limitaciones de la máquina no permitían hacer algo excesivamente grande y,
por otro lado, porque en general todos los sistemas domésticos acababan de llegar al mercado
y había pocas empresas desarrolladoras, con lo cual, el catálogo de software era aún muy
reducido y los usuarios siempre estaban ansiosos de probar nuevos títulos en su ordenador
personal.
A medida que evolucionaba la tecnología y la competencia inundaba el mercado con
productos de calidad creciente, se hacía más necesario contar al menos con un pequeño equipo
Figura 0.1. Livingstone Supongo, 1986, creado por el estudio español Opera Soft
Daniel Ponsoda Montiel
xxi
de personas especializadas en su campo (programación, gráficos, sonido, diseño) para
desarrollar un videojuego.
Al mismo tiempo, los desarrolladores fueron creando también herramientas y definiendo
nuevas técnicas con el objetivo de realizar productos con el mínimo tiempo y esfuerzo para así
poder seguir siendo competitivos. A raíz del desarrollo de varios videojuegos, los
programadores han ido identificando y aislando en forma de librerías elementos comunes
reutilizables en todas sus producciones, como rutinas de pintado de sprites, inteligencia
artificial, etc. y además también herramientas que les ayudan en su trabajo formando entornos
de desarrollo completos enfocados a videojuegos.
De la combinación de todos estos elementos surgen los motores de juegos (o game engines)
y no al revés como muchos nuevos desarrolladores podrían pensar. Las empresas
desarrolladoras más importantes normalmente han ido creando siempre un motor propio que
han ampliado y mejorado constantemente con cada título que han producido. Esto les ha
proporcionado una identidad y un toque personal que en muchas ocasiones han mantenido
siempre celosamente bajo llave.
No obstante, actualmente la tendencia es el open source y el apoyo de la comunidad
mediante mejoras al código base o al menos a través de plugins para motores con licencias
privativas, ya que se ha demostrado que la comunidad de desarrolladores ofrece un inmenso
potencial creativo que no podemos desaprovechar si queremos seguir siendo competitivos.
Ejemplos populares de esta tendencia abierta que se utilizan en juegos comerciales son los
motores Unreal Engine, CryEngine, o IdTech y recientemente también Unity3D.
Game engine multiplataforma
xxii
Dicho esto, para aquellos que quieren aprender a programar realizando un videojuego es
mucho más aconsejable NO utilizar un motor existente, sobre todo si lo que quieren en un
futuro es dedicarse profesionalmente a esto. La mejor forma de aprender sería crearlo todo
desde cero utilizando un lenguaje de propósito general (por ejemplo, C++). Esto les hará
plantearse cuestiones que ya tendrían resueltas si usan un motor, pero que es necesario que
sepan resolverlas por sí mismos de forma que cuando utilicen un game engine puedan tener
una mejor visión general y saber por qué las cosas se han diseñado de cierta manera.
La ventaja que tenemos actualmente (y también el inconveniente) con respecto a cómo
empezaron los primeros programadores es la inmensa cantidad de información que tenemos al
alcance de un click en Internet. Siempre encontraremos alguien que ha tenido el mismo
problema que nosotros y que nos puede ayudar, pero ojo, siempre es mejor intentar resolver
el problema por nosotros mismos para ejercitar nuestra capacidad resolutiva.
Por otro lado, un error bastante común que podemos cometer siguiendo esta vía al hacer
un juego desde cero es crear primero el motor sobre el que funcionará. Como ya hemos visto,
un game engine surge por si solo de la experiencia del diseño y realización de sucesivos
videojuegos. Cuando lo que queremos es realizar un videojuego, debemos centrarnos en nuestro
objetivo y hacer que funcione. Por supuesto no podemos esperar hacer una superproducción
Figura 0.2. Juego Crysis 3. Realizado por Crytek con su propio motor CryEngine
Daniel Ponsoda Montiel
xxiii
en nuestro primer intento, sino que lo mejor sería empezar por un juego lo más sencillo posible
que sepamos que seremos capaces de terminar.
En cualquier caso, nuestro segundo proyecto estará mejor diseñado y habremos empezado
a construir una pequeña librería de funcionalidades generales para desarrollo de videojuegos
que podrá ir ampliándose y mejorándose con cada nuevo juego ¿Quién sabe si finalmente se
convierte en un motor comercial? Pues como veremos en este documento, ningún engine actual
es perfecto. Todos tienen inconvenientes que podemos tratar de mejorar si partimos desde el
principio con una visión diferente.
Acerca de la industria de los videojuegos En la actualidad los videojuegos son el principal sector en la industria del entretenimiento
en cuanto a volumen de negocio. Por tanto, no es de extrañar que, desde hace ya varios años
mueva más ingresos que el cine y la música juntos. Y es que, al contrario de lo que se solía
asociar a este sector en sus primeros tiempos, en la actualidad está dirigido al público de todas
las edades, y no solo a niños. En realidad, son los adultos quienes más dinero gastan en este
tipo de producto.
Siempre atendiendo a una justa medida y un uso responsable, los videojuegos ejercitan
enormemente la capacidad de resolución lógica, la atención y los reflejos de los jugadores y,
por otro lado, se sabe que ayudan en la rehabilitación a personas con necesidades especiales
como autismo, Alzheimer y otras enfermedades neurológicas o psicomotrices.
Además, se trata de un campo multidisciplinar que abarca no sólo tecnología, sino también
arte gráfico, música, diseño de lógica de juego, guion, etc. Por tanto, se presenta muy atractivo
para infinidad de profesionales de multitud de campos que lo tienen muy claro a la hora de
invertir su esfuerzo. De hecho, existe tal carrera por abrirse paso en este mercado que las
inversiones en los juegos de última generación son multimillonarias.
El gráfico que se muestra a continuación es un extracto del estudio realizado por Newzoo
(Newzoo, 2018) y es una estimación de los beneficios interanuales que generarán los
Game engine multiplataforma
xxiv
videojuegos para abril de 2017 según sector (PC, móviles y consolas). Las cifras están indicadas
en miles de millones de dólares (billones en Norteamérica).
Figura 0.3. Beneficios interanuales de videojuegos según Newzoo
El total es de 108,9 mil millones de dólares. Además, como vemos a continuación, según
este mismo estudio, se estima que esta cifra seguirá creciendo a un ritmo constante hasta
llegar a los 128 mil millones en 2020, siendo el sector de los juegos móviles el que más
rápidamente crecerá.
Figura 0.4. Crecimiento del mercado de videojuegos según Newzoo
Daniel Ponsoda Montiel
xxv
Además de los mencionados sectores por plataformas, el mercado también se divide por
categorías según su grado de inversión. En este caso podríamos distinguir el sector indie y el
sector profesional cuyos juegos son conocidos en ocasiones como triple A. Para este último
caso, el presupuesto para desarrollar un videojuego puede llegar a alcanzar cifras que situamos
entre los 55 y los 285 millones de dólares (Wikipedia, 2018).
Estos números se justifican si tenemos en cuenta que los efectos gráficos, guiones, música,
voces dobladas y demás características que conforman un videojuego actual no tienen nada
que envidiar a los que se realizan para las películas de Hollywood.
Esto es una buena noticia para los desarrolladores, ya que las grandes compañías no suelen
escatimar en gastos cuando se trata de ofrecer un buen salario a los profesionales que poseen
las competencias que demandan.
En la siguiente tabla (Texterity, 2011) podemos ver el salario medio para Estados Unidos,
Canada y Europa según su disciplina dentro de la cadena de desarrollo:
Figura 0.5. Salario de desarrolladores de videojuegos según Texterity
Como vemos, incluso en Europa, con un 9% de los encuestados procedentes de España, el
salario medio para un programador se sitúa en torno a los 48.000 dólares anuales, con lo que
podemos afirmar que se trata de un sector que ofrece unas condiciones más que dignas a
quienes se dedican a él.
Daniel Ponsoda Montiel
27
1 INTRODUCCIÓN
1.1 Objetivos El objetivo de este proyecto es la realización de un game engine, así como el estudio de los
fundamentos básicos de los principales componentes que conforman este tipo de sistemas. Lo
primero puede parecer una contradicción a las recomendaciones comentadas en la
introducción. No obstante, hay varias cuestiones fundamentales que justifican la creación de
este motor y que pasamos a comentar a continuación.
En primer lugar, por su arquitectura, como veremos en el siguiente apartado, el motor que
vamos a realizar plantea una forma de trabajo que reduce drásticamente el tiempo de
desarrollo con respecto a los motores más utilizados actualmente como por ejemplo Unity3D
o Unreal cuando la plataforma objetivo son los dispositivos móviles. Con lo cual nos servirá
para experimentar con ciertas características innovadoras.
En segundo lugar, no partiremos completamente desde cero, sino que integraremos
distintas librerías de apoyo de código abierto para los apartados de gráficos, física e
interpretación de scripts (todo esto se detallará más adelante en esta memoria).
Por último, quien escribe ya tiene experiencia en el desarrollo de videojuegos, con varios
títulos publicados en el mercado de Android e iOS, así que no iremos completamente a ciegas
a la hora de plantear la estructura general del motor.
1.2 Motivación La motivación principal de este motor de videojuegos es proporcionar un sistema de
desarrollo multiplataforma que acelere el modo en que actualmente ajustamos y probamos los
videojuegos durante el desarrollo, especialmente en plataformas móviles.
Game engine multiplataforma
28
Cuando hacemos un nuevo juego, algo muy habitual es ajustar constantemente cuestiones
como la velocidad de los enemigos, la altura del salto de un personaje, etc. hasta llegar a
definir experimentalmente la jugabilidad que buscamos por medio de múltiples ajustes y
pruebas. Para ello, tradicionalmente los pasos a seguir son los siguientes:
El primer inconveniente que vemos en el anterior diagrama es el hecho de tener que
compilar el programa por cada pequeño ajuste del juego que queremos probar. Esto puede
ralentizar sensiblemente nuestro ritmo de trabajo. Sin embargo, este es un paso necesario de
los lenguajes compilados como C++.
Para un videojuego donde la el tiempo de ejecución es en casi todo momento crítico, no
podemos prescindir de este tipo de lenguajes. Incluso, para las secciones más críticas suele
emplearse ensamblador, puesto que necesitamos generar código lo más optimizado posible para
estar a la altura de los niveles de eficiencia y calidad exigidos para competir en el mercado
actual.
La solución a estos inconvenientes en el proceso de desarrollo y pruebas en los videojuegos
es el principal objeto y motivación de este proyecto.
1. Hacer cambios
2. Compilar
3. Ejecutar
4. Probar
¿ok?
Si
No
Por ejemplo, aumentar la velocidad de los enemigos.
Puede tardar más o menos dependiendo del tamaño del programa y el lenguaje utilizado (podemos hablar de segundos o incluso minutos)
Podemos hacer que se ejecute directamente desde el punto que queremos testear, aunque esto no siempre es trivial e implica cambios temporales en el código que hay que deshacer
Verificamos que el juego se ajuste a los requisitos de manejo, jugabilidad y dificultad definidos.
Si el cambio no nos satisface, volvemos al primer paso
Figura 1.1. Proceso general de ajuste y pruebas
Daniel Ponsoda Montiel
29
1.3 Solución inicial En contraste con C++, existen los lenguajes de script, que funcionan a través de una
máquina virtual que los interpreta y ejecuta directamente desde el código que escribimos sin
necesidad de compilar. Desafortunadamente, esta forma de ejecución es al menos 10 veces más
lenta que la tradicional. Sin embargo, podemos considerar hacer una combinación de ambas
aproximaciones aprovechando lo mejor de cada una.
Sabemos que la parte más cambiante del juego va a ser el diseño de la lógica y los pequeños
ajustes en la jugabilidad. Además, esta parte es generalmente la menos crítica, ya que no
requiere de complejos cálculos matemáticos ni se suele ejecutar de forma constante. Así pues,
la mejor solución para evitar recompilar innecesariamente es embeber en el propio motor una
máquina virtual que interprete un lenguaje de script. De esta manera, el núcleo del motor y
sus distintas rutinas más críticas como el dibujado de gráficos o simulación de físicas, entre
otras, pueden permanecer optimizadas usando C++, mientras que las cuestiones más
cambiantes como la lógica del juego se escribirán usado un script, para que sean más rápidas
de modificar.
Haciendo esto podemos eliminar del diagrama anterior los pasos 2 y 3, reduciéndose el
proceso a simplemente “cambiar y probar”, ya que la máquina virtual de la que hablamos
tendría la capacidad de modificar el código en tiempo de ejecución sin detener el programa.
Siendo así, un primer boceto general de la arquitectura del motor podría ser el siguiente:
Lógica del juego (scripts)
Intérprete de scripts
Núcleo del motor (C++)
Gráficos Colisiones Física Sonido …
API de acceso a hardware
Figura 1.2. Boceto inicial de la arquitectura del motor
Game engine multiplataforma
30
1.4 Problema en plataformas móviles Cuando realizamos un juego para móviles, es muy común la necesidad de hacer los ajustes
sobre el propio dispositivo. Esto es así porque podemos necesitar probar el juego utilizando
los elementos de control y visualización característicos de los smartphones, como son la
pantalla multitáctil (imposible de imitar con el ratón), o el control de componentes del juego
usando los acelerómetros, giroscopios u otros sensores.
Por supuesto no nos basta con simularlo en el PC por medio de teclas o demás atajos, ya
que cuando desarrollamos un juego es crucial comprobar en todo momento que el sistema de
control que estamos creando es fácil de usar y es jugable. En definitiva, necesitaremos realizar
los ajustes sobre el hardware para el que va destinado el juego si queremos probar la
experiencia real que ofrece lo que estamos desarrollando.
Los pasos vistos anteriormente para cambiar el código, ejecutar y probar se pueden
complicar bastante cuando la máquina de desarrollo es distinta de la de pruebas. desarrollamos
en el PC para luego pasar el programa creado al dispositivo de pruebas. Esta circunstancia
complica enormemente el problema. Los pasos ahora son:
1. Hacer cambios en el código para ajustar cuestiones de jugabilidad 2. Generar el paquete de la aplicación (por ejemplo el .apk) 3. Transmitirlo al dispositivo 4. Instalar el programa 5. Ejecutar el programa 6. Probar los cambios 7. Volver al paso 1 hasta que el resultado sea satisfactorio
No importa que hayamos usado un lenguaje de script. Igualmente tendremos que realizar
los pasos más pesados en cada prueba (desde el 2 al 5) que son generar y transmitir el paquete,
y luego instalar y ejecutar el programa sobre el dispositivo real.
Al principio es un proceso relativamente rápido (aproximadamente un par de minutos
entre una ejecución y otra), pero a medida que el juego crece, no solo lo hace en cuestión de
código, sino también en cuanto a los archivos que utiliza (imágenes, modelos 3D, sonidos,
música, etc.). Todos estos archivos deben volver a empaquetarse y transmitirse junto con todo
Daniel Ponsoda Montiel
31
el .apk cada vez que queremos hacer cualquier cambio en la aplicación. Por este motivo, el
tiempo para realizar estos pasos puede llegar a ser de 5, 10 o más minutos en juegos de tamaño
medio o grande.
1.5 Solución para plataformas móviles La propuesta se ilustra en la siguiente figura y consiste en dividir el sistema de pruebas
para distribuirlo en red entre un PC (plataforma de desarrollo) y un Móvil (plataforma
objetivo). El PC donde trabajaremos tendrá la parte de edición del juego donde escribimos el
código, mientras que el móvil tendrá el reproductor, que estará a la escucha e interpretará y
ejecutará las órdenes que le enviemos, actualizando en consecuencia su estado y
comportamiento.
De esta forma podremos hacer cambios y probarlos instantáneamente sin tener que volver
a crear, transmitir, reinstalar y ejecutar el paquete en cada prueba.
Esto mejorará en gran medida la productividad del desarrollador de juegos y permitirá
hacer incluso un mayor número de pruebas ayudándole a centrarse en su trabajo y a observar
todas las posibilidades del juego que está desarrollando.
Además, esta solución no tiene por qué limitarse solo a móviles, sino que es perfectamente
extrapolable a la programación para todo tipo de consolas y en general cuando el sistema
objetivo es distinto al que se utiliza para desarrollar.
Editor del juego Reproductor
Órdenes script
Figura 1.3. Esquema de trabajo editor/reproductor
Game engine multiplataforma
32
Como último apunte, será también conveniente crear una versión especial de depuración
del motor que permanezca a la escucha de nuevo código por parte del PC y lo ejecute cuando
llegue. Esta versión será distinta de la que se usará para distribuir el juego, la cual estará
desprovista de esta característica con el fin de evitar posibles intrusiones por parte de un
hacker.
1.6 Alcance del proyecto En este proyecto construiremos la infraestructura necesaria para implementar la
funcionalidad descrita en los anteriores apartados. Para ello integraremos los siguientes
componentes, apoyándonos siempre que sea posible en librerías existentes:
Módulo de gráficos. Necesario para renderizar los elementos visuales del juego en
pantalla. Deberá ser capaz de renderizar gráficos 3D con efectos visuales básicos como:
- Mapeado de texturas
- Iluminación direccional y de punto
- Sombras
Módulo de física. Utilizado para simular leyes de dinámica de cuerpos rígidos en el
juego y dar así un movimiento realista a los objetos de la escena. Se necesitarán las
siguientes características:
- Dinámica de cuerpos rígidos
- Detección de colisiones para formas básicas y objetos convexos
- Colisiones
Módulo de scripting. Pieza fundamental para implementar la lógica del juego aplicando
la principal característica del motor que es la agilidad de desarrollo.
Núcleo del motor. Se encargará de orquestar todos los elementos y comunicarlos entre
sí y con el usuario.
Entorno de desarrollo del motor. Incluirá el editor visual de objetos y escenas y editor
de código.
Como requisito común para todos los componentes, necesitaremos que sean lo más
eficientes posible para tratar al menos de aspirar al nivel técnico de motores actuales.
Daniel Ponsoda Montiel
33
Todo se desarrollará de forma que sea portable, y se compilarán versiones del motor y el
reproductor al menos para PC (Windows o Linux) y Android, mientras que el editor se
compilará solamente para PC.
Daniel Ponsoda Montiel
35
2 ESTUDIO DE GAME ENGINES EXISTENTES
En este apartado vamos a realizar un estudio de las alternativas más interesantes que
existen actualmente con el fin de aprender de sus principales características y analizar sus
ventajas y sobre todo sus inconvenientes. De esta forma podremos tener un mejor criterio a
la hora de tomar decisiones para el diseño de nuestro motor.
El estudio se realizará sobre los motores Unity3D de Unity Technologies, Unreal Engine
de Epic Games y CryEngine de Crytek. Sin embargo, no vamos a entrar en demasiados detalles
concretos sobre cómo se usan ya que nuestro objetivo principal es simplemente aprender de
sus aspectos distintivos.
2.1 Unity3D Comenzamos sin duda alguna por el más utilizado entre los desarrolladores indie. Unity3D
está desarrollado por la compañía californiana Unity Technologies y, desde su publicación en
mayo de 2005, no ha parado de evolucionar y obtener reputación y ventas.
Siempre ha sido de código cerrado, pero recientemente Unity se ha sumado a los muchos
estudios que ya tienden a una política más abierta y desde mayo de 2018 está publicado en
GitHub todo el código fuente (Unity, 2018). Además, es posible extender el motor y el editor
mediante un sistema de plugins extremadamente sencillo y versátil. Y es de esta extensibilidad
de donde surge principalmente su gran éxito.
El apoyo de la comunidad es tan fuerte que el motor dispone de la conocida como Asset
Store, que es un respositorio de plugins y assets clasificados por categorías donde podemos
encontrar cualquier cosa que podamos imaginarnos. Es muy difícil plantear algo nuevo que no
haya sido creado ya por alguien y que no esté publicado en el Asset Store.
Game engine multiplataforma
36
El sistema que controla la escena de juego está implementado utilizando el patrón entidad-
componente. Lo cual promueve la facilidad de edición en tiempo de ejecución.
2.1.1 El editor El editor es muy sencillo de utilizar cuando nos habituamos a él, y la curva de aprendizaje
evoluciona a buen ritmo si tenemos cierta experiencia general en el desarrollo de videojuegos.
La siguiente imagen es una captura del interfaz de edición.
Figura 2.1. Interfaz del editor de juegos de Unity3D
El interfaz se divide en 4 ventanas principales que pasamos a describir a continuación.
La ventana Hierarchy (jerarquía) nos da una vista de árbol de los elementos que forman
la escena actual identificados por sus respectivos nombres. Esto nos es útil para encontrar y
seleccionar rápidamente cualquier elemento del juego.
Como podemos deducir de esta estructura de árbol, los elementos pueden estar anidados
unos dentro de otros. Esto significa que las transformaciones que apliquemos como escala,
rotación y traslación sobre una entidad padre se aplicarán en cascada en toda la rama de
forma lineal.
En la zona centro superior tenemos la ventana Scene, que no es más que un visor donde
podemos ver el aspecto que va teniendo nuestro juego a medida que editamos. La cámara
Daniel Ponsoda Montiel
37
desde la que vemos la escena no pertenece al juego y podemos moverla sin miedo para ver
cualquier parte que estemos editando. Cuando lanzamos el juego se cambia automáticamente
a la pestaña Game donde sí que veremos el juego desde la cámara actual perteneciente a la
escena según la situación.
La ventana Inspector (Figura 2.2. Ventana
Inspector de Unity3D) nos permite editar todas las
opciones disponibles para el objeto actualmente
seleccionado. Dependiendo de su tipo, estas
opciones pueden ser muy diferentes. Un aspecto
muy interesante es que podemos asociar un script
a cada elemento el cual es una clase C#. Si dicha
clase tiene atributos públicos, podemos editar sus
valores en tiempo de ejecución desde la ventana del
inspector sin tocar el código. Esto es muy
conveniente puesto que podemos hacer esto por
cada instancia de la clase que se encuentre en la
escena y que podemos localizar fácilmente
mediante la ventana Hierarchy, lo cual facilita
enormemente las tareas de ajuste y pruebas.
En la parte inferior de la pantalla podemos encontrar la ventana Project, donde
podemos gestionar los assets (archivos de recursos) de nuestro juego. Nos ofrece también una
vista de árbol con la estructura del directorio de disco donde tenemos el proyecto. Al hacer
click en un elemento podemos ver en la ventana de la derecha la lista de assets que contiene,
que pueden ser modelos 3D, archivos de sonido,
imágenes, y demás recursos.
Unity ofrece la posibilidad de crear formas geométricas básicas para nuestra escena como
cubos, esferas, planos, etc. pero la mayoría de estos elementos no se crean mediante el editor.
Por ejemplo, los modelos 3D para los personajes se crean mediante software de diseño 3D
Figura 2.2. Ventana Inspector de Unity3D
Game engine multiplataforma
38
especializado de terceros como 3D Studio MAX o Blender, por tanto, existen infinidad de
formatos para cada tipo de asset por lo que es necesario importarlos en cada caso a un formato
común interno del motor para poder trabajar con ellos. Este proceso de importación es
realizado por Unity automáticamente al arrastrar un nuevo elemento al directorio de trabajo.
Además, cuando se producen cambios, el sistema lo detecta automáticamente y reimporta el
elemento en tiempo real.
Además de todas estas ventanas de propósito específico, disponemos también de opciones
de configuración global para todo el juego donde podemos determinar cuestiones como la
calidad general de los gráficos, plataformas objetivo, configuración del comportamiento del
editor, etc.
2.1.2 El sistema de scripting Como se ha mencionado, el código que escribimos para programar la lógica del juego es
C#, y cada script que escribimos representa una clase que puede ser asociada a cualquier
entidad de la escena.
Estas clases representan una entidad dentro de la escena. El patrón utilizado es una
aproximación orientada a objetos del entity-component-system (ECS). Con este patrón,
podemos construir fácilmente tipos de objetos complejos cuyo comportamiento se definirá por
los componentes que contenga y que además podrán ser añadidos en tiempo de ejecución. Esto
es algo que no sería posible con otros enfoques más tradicionales de programación orientada a
objetos como, por ejemplo, si el comportamiento se definiera por medio de una jerarquía de
herencia.
Estos componentes tendrán siempre una finalidad específica (siguiendo el principio de
responsabilidad única) que iremos incorporando a nuestras entidades según los necesitemos.
Para tener una idea de lo que hacen estos componentes, describimos a continuación algunos
ejemplos:
Transform: Contiene la información para transformar el objeto (posición, escala y
rotación), es decir, la matriz de transformación, vectores de posición y escala y el
Daniel Ponsoda Montiel
39
cuaternión para la rotación. Se encarga de aplicar las transformaciones en cascada
siguiendo la posición del objeto en jerarquía de la escena.
Mesh: Representa la malla 3D del objeto que se dibujará en pantalla.
Material: Contiene las texturas, y los shaders usados para dar el aspecto deseado a la
superficie del objeto.
Rigidbody: Representa la forma del objeto (independiente de su forma real) que se
usará para los cálculos de dinámica de cuerpos rígidos para simular la física en el juego.
Normalmente se usará una forma geométrica más sencilla que la que contiene el
componente Mesh, con el fin de acelerar los cálculos a costa de un menor realismo.
Por supuesto existen muchos otros, cada uno con su propio propósito como Audio, Collider,
GUILayer, Input, Billboard, ParticleEmitter.
2.1.3 Ventajas e inconvenientes Como resumen de las ventajas de Unity3D podemos destacar las siguientes:
La favorable curva de aprendizaje permite introducirse en el desarrollo de videojuegos
a desarrolladores de todos los niveles, lo que lo hace muy atractivo para equipos indie.
Dispone de players del motor para hasta 25 plataformas como Windows, Linux, Mac,
Android, iOS, PlayStation, Xbox, Wii, Nintendo Switch, entre otras, con lo que
nuestras creaciones podrán ser portadas a cualquier sistema actual con un solo click.
Extensibilidad total mediante un sistema de plugins magistralmente implementado
que proporciona un interfaz muy fácil de extender para casi cualquier propósito.
El excelente sistema de organización y composición de la escena permite realizar un
juego sencillo en cuestión de horas, o incluso minutos, mientras que a su vez facilita
la mantenibilidad en proyectos grandes.
El inspector supone una ayuda que no tiene precio a la hora de hacer ajustes sobre el
juego, ya que en combinación con la ventana hierarchy permite manipular de forma
visual cualquier elemento instanciado en la escena en tiempo real.
El sistema automático de importación nos abstrae de los pormenores a la hora de
integrar nuestros assets creados desde fuera de Unity. Simplemente guardamos los
Game engine multiplataforma
40
cambios en el otro programa y Unity lo detectará y reintegrará en nuestra escena sin
necesidad de intervenir por nuestra parte.
Pero por supuesto también tiene varios inconvenientes, de los cuales destacamos los más
importantes:
No tiene un editor ni compilador de código propio, con lo que para editar los scripts
en C# es necesario utilizar una herramienta externa capaz de compilar este lenguaje,
como Visual Studio o Monodevelop. Además, el hecho de que no esté integrado en el
editor principal puede provocar despistes al principio si tras hacer un cambio en el
código no nos acordamos de compilar en la ventana del otro IDE antes de ejecutar en
la de Unity3D.
El sistema de ajuste y pruebas del juego puede hacerse eterno cuando la plataforma
objetivo es por ejemplo Android o iOS, debido al hecho de que por cada prueba
debemos compilar los scripts (a bytecode más eficiente), crear el paquete, transmitirlo
al dispositivo, instalarlo y ejecutarlo.
Su extremada facilidad de uso a veces oculta detalles de implementación y cuesta saber
en ocasiones cuál es exactamente la forma más óptima de realizar ciertas tareas.
A pesar de que ofrece un enorme grado de control para tareas típicas, no siempre es
sencillo aplicar ciertos efectos o implementar características más avanzadas para
necesidades concretas, teniendo que recurrir a plugins de terceros.
2.2 Unreal Engine Creado en 1998 por Epic Games para su shooter en primera persona Unreal, este motor es
una auténtica leyenda para cualquier desarrollador de videojuegos, puesto que en su día ofreció
una calidad técnica nunca vista hasta entonces que permitió a esta compañía tener una
importante presencia en un mercado extremadamente competitivo.
Daniel Ponsoda Montiel
41
El motor ha sido mejorado continuamente y utilizado para todos los títulos de la saga del
juego Unreal. Actualmente va por la versión 4, la cual aporta cambios radicales con respecto
a la primera.
A diferencia de Unity3D cuyo objetivo parece estar más enfocado al mercado indie, el
motor Unreal ha sido utilizado en proyectos de empresas de la talla de Electronic Arts, Disney,
Atari, Ubisoft o Konami por citar sólo algunas.
Además, su código es abierto desde hace tiempo, lo cual nos proporciona como
desarrolladores un valor añadido de estudio, dándonos acceso a los detalles de implementación
de uno de los motores más avanzados tecnológicamente en la actualidad.
En general Unreal nos ofrece mayor grado de control sobre nuestros juegos que Unity, y
por este motivo es la herramienta preferida para títulos comerciales triple A.
2.2.1 El editor Las diferencias en el interfaz del entorno de desarrollo con respecto al de Unity3D (Epic
Games, 2018) se ilustran en las siguientes capturas:
EDITOR EN UNITY3D:
Game engine multiplataforma
42
EDITOR EN UNREAL ENGINE:
Figura 2.3. Comparativa de editores entre Unity y Unreal
En cuanto a interfaz del editor, en la mayoría de casos simplemente hay un cambio de
nombre de cada uno de los conceptos vistos en Unity. En las capturas anteriores se asocian
con el mismo color las correspondientes funcionalidades equivalentes de cada motor. La Scene
de Unity, en Unreal se llama Viewport, y de la misma forma, la ventana Hierarchy es ahora
el World outliner, la ventana Project es el Content Browser y el Inspector es la ventana
Details.
No obstante, sí que podemos ver una nueva ventana que no existe en Unity y es la venana
Modes. Esta característica nos permite definir de forma rápida el modo de edición lo cual
habilita a su vez otros interfaces especializados. Los modos son:
Place mode: Para añadir actores (entidades con comportamiento) a la escena
Paint mode: Permite pintar al vuelo la superficie de los objetos sobre el viewport,
modificando el color de los vértices en una malla o incluso su mapa de textura.
Landscape mode: Para edición de terrenos. Permite definir desniveles sobre
superficies de forma muy cómoda.
Foliage mode: Añade vegetación sobre una escena. Incluye múltiples opciones de
personalización. Ideal para editar paisajes sobre un terreno previamente definido en el
modo Landscape.
Geometry mode: Permite editar los vértices de una malla 3D.
Daniel Ponsoda Montiel
43
2.2.2 El sistema de scripting Basado también en un patrón ECS, en Unreal tenemos dos opciones para programar la
lógica del juego y ampliar funcionalidades: Por medio de C++ o utilizando Blueprints.
El API de C++ nos da un alto grado de control y optimización del código en bajo nivel,
mientras que con Blueprints podemos definir comportamiento en nuestro juego sin recompilar,
pero no por medio de scripts tradicionales tal y como los conocemos, sino utilizando un
innovador sistema visual. Quienes hayan programado shaders para gráficos 3D utilizando
software como Blender o 3D Studio Max encontrarán este sistema muy familiar.
Blueprints es un método para programar sin necesidad de escribir ni una sola línea de
código. Se basa en un sistema de nodos que procesan información y cuyas entradas y salidas
podemos interconectar para programar cualquier tipo de funcionalidad. A continuación, se
muestra una captura del editor de Blueprints:
La intención principal de este sistema es permitir que los miembros no técnicos del equipo
de desarrollo (por ejemplo, el diseñador del juego) puedan, sin conocimientos de programación,
hacer cambios y ajustes sencillos en el juego para hacer pruebas rápidas sin que tenga que
Figura 2.4. Sistema de Blueprints de Unreal Engine
Game engine multiplataforma
44
intervenir un programador constantemente. Esto agiliza el flujo de trabajo y reduce el tiempo
de desarrollo y promueve la buena calidad del producto.
2.2.3 Ventajas e inconvenientes En Unreal Engine destacan las siguientes ventajas:
Alto grado de control por medio del API en C++. Permite a los desarrolladores de
juegos programar características avanzadas para sus juegos e innovar, por ejemplo,
con nuevos tipos de efectos visuales o de física, etc. Además, al ser C++, es posible
reutilizar código que ya tengamos o integrar cualquiera de las miles de librerías que
existen escritas en este lenguaje.
El sistema de Blueprints permite intervenir a los diseñadores en cuestiones de ajuste
de la jugabilidad, de forma que cada profesional puede dedicarse a su especialidad. Por
ejemplo, un programador tiene el conocimiento técnico que le permite implementar y
optimizar efectos visuales, pero no tiene por qué saber hacer que el juego sea divertido
o siquiera jugable. Esta tarea ahora la puede hacer directamente el diseñador.
Su Marketplace proporciona una fuente de recursos de todo tipo para incorporar a
nuestros proyectos.
En cuanto a los inconvenientes para este motor no son demasiado importantes, pero
podríamos mencionar los siguientes:
El apoyo de la comunidad, aunque es bastante fuerte, y podemos encontrar fácilmente
cualquier información en Internet, es menos popular que Unity, con lo cual la cantidad
y diversidad de material que podemos encontrar en su Marketplace aún no puede
rivalizar del todo con el Asset Store de Unity3D, por tanto, muchas cosas a las que
tiene acceso Unity, si trabajamos con Unreal deberemos crearlas nosotros desde cero.
Su curva de aprendizaje es cómoda al principio, pero a veces cuesta más aprender a
crear características más avanzadas. No obstante, esto es debido a que nos permite
mucha más libertad que otras alternativas, con lo cual a la larga se convierte en una
ventaja.
Daniel Ponsoda Montiel
45
El sistema de Blueprints no suele verse con buenos ojos por aquellos que hemos
aprendido a programar escribiendo código, y en la mayoría de ocasiones cuesta cambiar
la mentalidad para adquirir agilidad con este sistema.
Aunque es multiplataforma no llega al número de plataformas compatibles de Unity,
si bien sí que puede portarse a los principales sistemas operativos (Windows, Linux y
Mac), móviles (Android y iOS) y videoconsolas actuales (PS4, XBoxOne y Switch).
2.3 CryEngine CryEngine vio la luz por primera vez en 2002 por la empresa alemana CryTek con el apoyo
de Nvidia para el desarrollo del juego Far Cry. Nvidia utilizó este motor para demostrar todas
las capacidades técnicas de su nueva generación de chips de aceleración de gráficos. Es por
este motivo que CryEngine se ha caracterizado siempre por aprovechar al máximo el hardware
y ofrecer espectaculares gráficos en tiempo real de la más alta calidad técnica disponible hasta
la fecha.
Con el tiempo se seguido mejorando y la versión más utilizada ha sido la 3, publicada en
2009 y con la que se han realizado docenas de juegos comerciales de última generación, como
la saga Crysis, Evolve, o Rise: Son of Rome por mencionar los más conocidos.
Actualmente está en la versión V (de 2016), la cual aún no tiene ningún juego publicado,
pero ya podemos descargarla y utilizarla puesto que es de código abierto al igual que los otros
motores que hemos analizado. Esto nos da acceso a todo un mar de conocimiento sobre las
últimas tecnologías que se han desarrollado en cuanto a computación gráfica, como el
Physically Based Rendering, el Voxel-Based Global Illumination (SVOGI), Sombras
volumétricas o técnicas de imagen HDR.
2.3.1 El editor El editor de CryEngine tiene una apariencia inicial bastante más simple que los de Unreal
y Unity, no obstante, a medida que trabajamos con él vemos que esconde un alto potencial
de edición, el cual nos permite no solo definir las escenas del juego sino también crear y
modificar todo tipo de objetos 3D posibilitando la creación de cualquier forma que necesitemos
sin necesidad de recurrir a un software de terceros.
Game engine multiplataforma
46
Tiene todas las características de los anteriores motores que hemos visto pero su interfaz
está más enfocada a la edición de entidades siendo éste su verdadero punto fuerte. Por ejemplo,
la siguiente captura muestra la ventana Designer Tool que nos ofrece un completo set de
herramientas integradas para la manipulación de objetos:
Figura 2.5. Editor de CryEngine
Figura 2.6. Interfaz de edición de objetos de CryEngine
Daniel Ponsoda Montiel
47
Entre estas herramientas podemos encontrar:
Creación de las típicas primitivas básicas 3D (cubo, cilindro, esfera, cono, etc.)
Creación de formas 2D como planos, círculos o curvas (útiles para definir trayectorias
kinemáticas para los objetos).
Selección y manipulación de objetos a nivel de vértice, cara, loop, etc.
Herramientas de extrusión, conexión, colapso, recorte, mezcla booleana, etc.
Y en definitiva todo lo que necesitamos para diseñar cualquier forma que imaginemos
en el espacio 3D de la escena.
2.3.2 El sistema de scripting Al igual que Unreal, este motor también dispone de un sistema visual para scripting
funcionando bajo un patrón ECS y con el nombre de Flow Graph. Desafortunadamente, no
esta característica en CryEngine no es tan potente como los Blueprints en Unreal.
La principal diferencia es que con Blueprints se puede hacer prácticamente de todo,
mientras que Flow Graph está más limitado a la implementación de mecánicas sencillas o al
control de eventos. Para el resto de situaciones deberemos implementar nuevos nodos escritos
en C++, con los inconvenientes y ventajas que esto conlleva.
Otra importante diferencia es que con Flow Graph no podemos generar código a partir de
los diagramas que hacemos (ni viceversa). Esta característica hace muy potente a Unreal,
porque una vez que tenemos la lógica creada con Blueprints podemos convertir todo a C++
para la versión para producción y así aprovechamos todo el potencial del código compilado.
Sin embargo esto no es posible por el momento en CryEngine.
2.3.3 Ventajas e inconvenientes Las principales características positivas de este motor son:
Al estar inicialmente apoyado por Nvidia, está enfocado para sacar el máximo
provecho de la aceleración por hardware. Además, esto no solo se limita a los gráficos,
sino que también aprovecha al máximo este tipo de hardware para realizar cálculos
para el sistema de físicas por medio del API de Nvidia PhysX.
Game engine multiplataforma
48
Implementa los últimos avances en computación gráfica para la aplicación de efectos
visuales.
Su interfaz de edición es muy sencillo de usar y muy completo, permitiendo hacer un
juego sencillo sin utilizar ninguna herramienta externa.
Por otro lado, las desventajas que podemos destacar son:
Su sistema visual de scripting tiene ciertas limitaciones funcionales y no permite la
conversión a C++ y viceversa.
Es ligeramente más difícil de dominar con respecto a Unreal y Unity.
Su orientación a NVidia lo hace ligeramente menos potente que otros motores cuando
la plataforma objetivo utiliza chips ATI.
Es el menos portable de entre los motores analizados. Los juegos creados con
CryEngine funcionan solo para Windows, Oculus Rift, Xbox One y PlayStation 4.
2.4 Mejoras que aportará nuestro motor Los motores que hemos estudiado son sistemas completos, optimizados y maduros,
realizados por equipos enteros de profesionales especializados en múltiples campos y mejorados
por la comunidad de desarrolladores, con lo cual nuestro motor no puede aspirar por ahora a
igualar la inmensa cantidad de funcionalidades que ofrecen.
No obstante, sí que podemos mejorar algunas de las desventajas mencionadas como, por
ejemplo:
El sistema de ajuste y pruebas será mucho más ágil, ya que permitirá enviar código al
dispositivo de pruebas sin tener que reconstruir el paquete. Esto reducirá
drásticamente el tiempo de desarrollo de juegos.
Nuestro entorno de desarrollo tendrá un editor de código integrado, con lo que podrá
configurarse para compilar antes de ejecutar y en el futuro podrá integrarse mejor con
un depurador especializado.
Daniel Ponsoda Montiel
49
3 MODELO DE DESARROLLO Y PLANIFICACIÓN
El modelo de desarrollo que se utilizará tomará conceptos y planteamientos conocidos de
los modelos ágiles, pero en realidad no seguiremos ninguno íntegramente, ya que gran parte
de la finalidad de éstos se basan en el trabajo en equipo. En cambio, este proyecto será
realizado por una sola persona (el gestor del proyecto, el desarrollador, el tester, el propietario,
etc. es el mismo), además no podremos realizar varias tareas en paralelo por lo que todos los
flujos de trabajo se ejecutarán de forma secuencial, así que podemos obviar algunas cuestiones
que, aparte de la carga que suponen, prácticamente no nos aportarían valor en nuestro caso.
3.1 Modelo iterativo El modelo que utilizaremos será iterativo, con iteraciones de tamaño fijo de 5 días hábiles
(1 semana natural). Manteniendo las iteraciones pequeñas y del mismo tamaño, minimizamos
errores de estimación temporal y mantenemos una rutina que nos permite comparar y aprender
de iteraciones anteriores.
Cada iteración dará como salida una nueva versión de desarrollo que podrá incorporar
corrección de errores y/o un nuevo incremento en la funcionalidad y nueva documentación.
El flujo de trabajo en estas iteraciones se estructura como se muestra en la siguiente figura:
Figura 3.1. Tareas de una iteración
Game engine multiplataforma
50
No obstante, no haremos el mismo tipo de tareas en todas las iteraciones, sino que al igual
que ocurre en UP (Unified Process) o en el modelo en espiral, podremos pasar por distintas
etapas (fases) del proyecto con distintos objetivos y por tanto habrá flujos de trabajo (diseño,
implementación, pruebas, documentación, etc.) con más peso que otros.
El diseño se hará previo a la implementación, pero se hará de forma general, sin entrar en
mucho detalle y sólo para la parte del código que se vaya a realizar, ya que estamos aplicando
un modelo ágil. El diseño final podrá ser revisado y modificado tras la implementación o al
refactorizar el código.
Las pruebas unitarias no siempre podrán llevarse a cabo debido a la naturaleza del
proyecto, y es que, al ser esencialmente una aplicación gráfica, salvo algunas excepciones, no
podremos automatizar las pruebas (los resultados son visuales), y las unidades que podrían
ser testables como las funciones matemáticas, de física, colisiones, red, etc. proceden de
librerías de terceros ya probadas. Sin embargo, sí que podremos automatizar pruebas de
integración o de sistema.
3.2 Planificación adaptativa Además de los tipos de tareas mencionados (análisis, diseño, implementación y pruebas),
tendremos otras tareas que también deberán ser realizadas a medida que avanza el proyecto,
como son la planificación de las iteraciones, la documentación o la configuración del entorno.
La planificación será adaptativa, lo que significa que no haremos un plan detallado previo
de todo el proyecto. En nuestro caso tendremos, por un lado, un plan general donde sólo
identificaremos los hitos principales y, por otro lado, un plan detallado con las tareas definidas
para las primeras 2 iteraciones (la actual y la siguiente).
Al final de cada iteración, se incluirá una tarea de cierre donde se hará una valoración del
trabajo realizado y se planificará una nueva, revisando los posibles errores de estimación y
ajustando los tiempos de las tareas si fuera necesario.
Daniel Ponsoda Montiel
51
3.2.1 Plan general Para el plan general establecemos los objetivos por iteración que se muestran en la
siguiente tabla. Disponemos de 180 horas para terminar el proyecto, por tanto, definiremos el
proyecto en 5 iteraciones asumiendo una dedicación de 40 horas por semana. No obstante, las
tareas se redistribuirán finalmente a lo largo del curso para poderlo compatibilizar con otras
asignaturas de la carrera.
Iteración 1 Análisis problema, estudio motores actuales y documentación preliminar
Iteración 2 Arquitectura y diseño del reproductor y el editor y primera versión
Iteración 3 Integración de los sistemas de gráficos y física
Iteración 4 Integración de los sistemas de script y red
Iteración 5 Implementación de las funciones del editor
3.2.2 Plan detallado A continuación, se muestra una captura de las tareas del plan detallado realizado con
Microsoft Project durante la iteración 2, donde podemos ver las primeras 3 iteraciones, con su
descripción, duración, fecha de ejecución y progreso.
Daniel Ponsoda Montiel
53
4 DEFINICIÓN DE LA ARQUITECTURA
4.1 Patrón Entidad-Componente-Sistema Nuestro motor seguirá el patrón arquitectural Entidad-Componente-Sistema, también
conocido abreviadamente como ECS. Se trata de un patrón fundamentado en el principio de
composición sobre herencia por el cual se trata de evitar la creación de complicadas jerarquías
de objetos en favor de la composición, la cual promueve el bajo acoplamiento y permite
construir objetos de forma más flexible y que pueden modificarse en tiempo de ejecución.
Este patrón se utiliza principalmente en videojuegos, ya que encaja perfectamente en el
modelo de motor que aparece de forma intuitiva, y que responde a un sistema formado a su
vez por varios subsistemas independientes (gráficos, física, sonido…) que interactúan de algún
modo.
A partir de aquí, una entidad en nuestro juego, que podría ser un personaje o un arma,
no es más que un contenedor de varios componentes, cada uno de los cuales procede de uno
(o incluso varios) de los sistemas mencionados. Estos componentes tendrán por tanto una
finalidad concreta por lo que además serán altamente cohesivos. Por ejemplo, un personaje
podría estar formado por:
Componente de transformación: Indica la posición, escala y rotación del objeto en el
espacio 3D que será usado, entre otros, por los sistemas de gráficos, colisiones y física.
Componente de gráficos: Sería una malla de un modelo 3D que representa una figura
humana a dibujar en la pantalla.
Game engine multiplataforma
54
Componente de colisiones: Primitiva o forma geométrica utilizada para determinar si
el personaje está en contacto con otro objeto de la escena, como un enemigo, etc.
Componente de física: Condiciones de masa, densidad, rigidez, etc. a tener en cuenta
a la hora de resolver las fuerzas que actúan sobre el personaje para desplazarlo a un
lado y/o modificar su postura de forma realista cuando impacta con un objeto.
Componente de sonido: Fragmentos de audio a reproducir según la situación.
Componente de IA: Código (scripts) con la lógica de comportamiento del personaje.
Cada sistema almacenará en una lista sólo aquellos componentes que sean pertenecientes
a su dominio, con lo que se mantiene el acoplamiento bajo.
Además, se cumple también el principio de responsabilidad única, ya que estos
componentes contienen poco más que información y los métodos para tratar con ella,
delegando a sus respectivos sistemas la responsabilidad de realizar las tareas necesarias. Por
ejemplo, un componente de gráficos tendrá la información de cómo es el objeto, pero no será
éste el responsable de pintarlo en pantalla, sino que será el sistema de gráficos el encargado
de esa tarea.
En nuestro motor tendremos los siguientes sistemas o tipos de componentes:
Transformación (posición, escala, rotación), Gráficos (modelo a renderizar), Colisiones
Figura 4.1. Ejemplo Entidad-Componente-Sistema
Daniel Ponsoda Montiel
55
(bounding volumes), Física (velocidad, aceleración…), Lógica (código Lua) y Entrada (entrada
de usuario). El diagrama en forma de rejilla que se muestra a continuación representa, un
ejemplo concreto de lo que pretendemos conseguir. Los círculos son instancias de componentes
de sus respectivos sistemas (representados con rectángulos):
Algo que debemos destacar es que los componentes están agregados a sus respectivos
sistemas, y no a las entidades como inicialmente se puede pensar. En realidad, no va a existir
un objeto entidad como tal, sino que una entidad será un concepto lógico para el cual sólo
necesitaremos un identificador para referirnos a él. De este modo, los sistemas almacenarán
los componentes en tablas hash donde se relacionan por su id, y este id representará y será
para nosotros la entidad.
Otro aspecto interesante es que, si nos fijamos en los ejemplos anteriores, vemos que los
contenedores son básicamente ranuras de datos que podrán almacenarse en la misma región
de memoria según dominio. Esto quiere decir que el sistema propicia un diseño guiado por
datos (data-oriented design), el cual nos permite aprovechar el principio de localidad para
optimizar el uso de la cache, evitando gran parte de los fallos de lectura/escritura (cache
misses).
4.2 Despliegue Como ya se ha mencionado, el proyecto se divide en dos grandes unidades: El reproductor
y el editor.
El reproductor es la parte que ejecuta y muestra el juego según la lógica programada,
mientras que el editor es una herramienta visual que permite al desarrollador de juegos editar
el proyecto que está realizando. Este editor tendrá como dependencia al reproductor, ya que
se utilizará para poder visualizar el progreso de nuestro trabajo y realizar pruebas.
Habitualmente necesitaremos trabajar con el editor en un ordenador de escritorio mientras
que podremos tener versiones autónomas del reproductor ejecutándose en las distintas
plataformas objetivo (por ahora sólo Android y PC para nuestro proyecto). El esquema de
despliegue podría quedar así:
Game engine multiplataforma
56
En la versión de desarrollo, estos reproductores estarán permanente a la escucha de nuevas
órdenes desde el editor para ejecutarlas en tiempo real mientras que, en la versión de
producción, esta funcionalidad estará deshabilitada y el comportamiento será el programado
en el paquete instalable.
4.3 Arquitectura del reproductor Siguiendo el patrón ECS, aplicamos algunos cambios a la arquitectura con respecto a la
idea inicial planteada en el apartado de introducción (ver 1.3 Solución inicial). Ahora el script
es sólo un componente más perteneciente a una entidad y se agregará a su correspondiente
sistema. Los datos y el comportamiento serán determinados por un gestor de la escena, el cual
será el encargado de actualizar los distintos subsistemas, incluyendo llamadas a los métodos
correspondientes de los scripts para actualizar el estado de la lógica.
Figura 4.2. Despliegue del editor y los reproductores
Daniel Ponsoda Montiel
57
El siguiente esquema muestra la nueva situación de la arquitectura:
Como se puede ver, debido a que los sistemas estarán implementados en C++,
necesitaremos una capa intermedia entre el sistema de lógica y el resto de subsistemas que se
encargue de adaptar la comunicación entre estos.
También, como podemos ver, existen para estos subsistemas distintos tipos de atributos o
componentes. Por ejemplo, el sistema de colisiones puede tratar con distintos tipos de
primitivas básicas o incluso con una malla de forma arbitraria. Será decisión del programador
de juegos qué tipo de volumen es más adecuado según el caso.
La entrada de jugador también podrá proceder de distintos dispositivos hardware y
podremos asociar uno o varios a una determinada entidad de la escena. En este componente
definiremos el mapeo de la entrada de hardware con determinadas acciones, por ejemplo, el
mapeo de la barra espaciadora con el disparo del personaje. Esto podrá redefinirse por el
usuario en tiempo de ejecución. Además, otro aspecto útil de tener este componente
desacoplado del resto, es que podemos sustituirlo por un grabador / reproductor que puede
sernos útil para grabar una jugada y servir posteriormente como una demo o incluso como
prueba funcional.
Figura 4.3. Arquitectura del player
Game engine multiplataforma
58
En cuanto al componente de transformaciones, podrán afectar a la posición, escala y
rotación, y además contendrán un nodo padre que indicará si la entidad está anidada en otra.
Esto hará que las transformaciones a aplicar sean relativas al padre multiplicando las
respectivas matrices en cascada de forma recursiva. Además, como ya hemos visto, este
componente se presenta como un intermediario que desacopla los sistemas de colisiones, física
y gráficos.
El API de hardware a usar dependerá de las dependencias utilizadas. En este caso, por
ejemplo, para los gráficos, tenemos Ogre que a su vez utiliza OpenGL (o DirectX/OpenGL en
Windows).
4.4 Formato y recursos del proyecto de juego Toda la información del juego a crear por nuestro motor se guarda en una carpeta de
proyecto que contiene todos los archivos de recursos (carpeta resoruces), así como otros
archivos especiales de información que definen las entidades (entities), las escenas (scenes), y
el proyecto (project.json). Para estos últimos se empleará el formato json con el fin de que
puedan también editarse manualmente y facilitar la interoperabilidad. La estructura de la
carpeta de proyecto se muestra a continuación:
resources Contiene archivos de script, texturas, modelos 3D, materiales, etc, que pueden ser usados por una o más entidades de nuestro juego. El desarrollador puede organizar esta carpeta libremente.
entities Cada entidad que podemos insertar en una escena se guarda en esta carpeta como como un archivo json que define sus componentes y propiedades por defecto.
scenes
En esta carpeta se guardan los json de las escenas. Estas contienen una estructura jerarquizada de entidades donde, para cada una, se especifican sus propiedades de instanciación concretas, como su posición, rotación, valores de los atributos públicos del script, etc.
project.json Contiene básicamente una lista de escenas y la configuración global del proyecto. La primera escena de la lista será la que se abrirá de forma inicial.
Daniel Ponsoda Montiel
59
5 DISEÑO DEL INTERFAZ
5.1 Interfaz gráfica del editor La siguiente imagen es un mockup de la pantalla principal del editor. La idea inicial no se
aleja del concepto de otros motores como Unity o Unreal, pero hay algunas diferencias clave
que explicaremos a continuación.
Figura 5.1. Mockup pantalla principal del editor
Game engine multiplataforma
60
Como vemos, se divide en 5 partes principales:
Escena: Sería el equivalente a la ventana Hierarchy de Unity y contiene el árbol de
objetos de la escena. Al hacer click sobre un elemento se selecciona en la escena la
entidad correspondiente, y la ventana propiedades muestra también los ajustes de esta
entidad.
Recursos: Es un selector de ficheros que muestra el árbol del directorio de trabajo.
Este selector nos ayudará a la hora de asociar recursos a los componentes de las
entidades, o para crear nuevas entidades pinchando y arrastrando un recurso a la vista
de la escena.
Propiedades: Aquí podemos editar las opciones específicas de cada objeto al igual
que la ventana Inspector de Unity. Podremos seleccionar el componente a editar desde
el correspondiente desplegable. Si un componente no existe para la entidad actual
aparecerá una opción para crearlo. También se podrá activar/desactivar la entidad
asociada. Una entidad desactivada no se muestra en pantalla y se ignora a todos los
efectos durante la ejecución.
Consola: Muestra mensajes de depuración procedentes de la ejecución de los scripts.
La diferencia con la consola de Unity es que aquí podemos escribir código que se
interpretará en tiempo de ejecución.
Área de edición: Situada en la parte central, nos permite ver la escena tal como se
mostrará en el juego y además, a diferencia de Unity, tiene un modo “Código” en el
que podemos editar el script del objeto actualmente seleccionado. Para cambiar de un
modo a otro sólo tenemos que hacer click en la pestaña correspondiente.
Los botones de Stop, Play y Pause que pueden verse en el mockup en la esquina superior
derecha sirven para detener, ejecutar y pausar la ejecución del proyecto en todos los
dispositivos actualmente conectados.
Menú Archivo
El menú archivo tendrá las opciones de Nuevo, Abrir, Guardar y Cerrar con respecto al
proyecto o a la escena, y también la opción de Salir del programa.
Daniel Ponsoda Montiel
61
Si se abre una escena se abrirá implícitamente el proyecto asociado.
El editor no será multiproyecto, es decir, si se abre o se crea un nuevo proyecto se cerrará
el actual. No obstante, si seleccionamos cualquier orden que implique cerrar un documento
donde tengamos cambios sin guardar se pedirá confirmación.
Menú Edición
Tendrá las opciones del portapapeles: Copiar, Pegar y Cortar que, en el modo Escena,
actuarán sobre la entidad actualmente seleccionada y, en el modo Código, actuarán sobre el
éste tal como lo haría cualquier editor de texto.
También tendremos opciones de Deshacer y Rehacer que se implementarán mediante el
patrón command.
Finalmente, el menú edición tendrá la opción Preferencias donde se podrá editar la
configuración general de la aplicación.
Menu Entidad
Desde aquí podremos añadir nuevas entidades a la escena. Las nuevas entidades creadas
aparecerán en la posición del cursor 3D, y tendrán asociados los componentes necesarios en
función de la naturaleza de la entidad. Estas podrán ser:
Primitivas: Esfera, Cubo, Cono.
Luz
Cámara
Entidad vacía
Menu Ayuda
Aquí se mostrarán enlaces al manual del API, el manual del editor y un apartado “acerca
de” donde aparecerá el nombre de la aplicación, versión, autor e información legal.
Game engine multiplataforma
62
5.2 Interfaz de consola La consola nos permitirá enviar código de script en línea o incluso archivos al dispositivo
de pruebas. El código enviado se ejecutará inmediatamente mediante el intérprete Lua en el
reproductor de la máquina destino.
El envío de archivos nos será útil para cuando hagamos cambios en assets en local y
queramos ver el resultado rápidamente sin tener que copiarlos a mano.
La consola se podrá utilizar desde el terminal del sistema operativo o bien desde una
ventana embebida en la aplicación del editor gráfico. En ambos casos se deberá especificar al
inicio la ip y puerto de conexión al dispositivo remoto al que vamos a conectar, así como la
ruta local a la carpeta del proyecto que estamos realizado.
Esta configuración se podrá establecer en la ventana de propiedades en el editor gráfico.
En el caso del terminal, se puede especificar en la línea de comandos. Para ejecutar la consola
en el terminal escribimos:
Console <ip> [puerto] [root]
Donde la ip es obligatoria. El puerto por defecto es 8080, pero se puede cambiar según
nuestra preferencia. El directorio de raíz del proyecto (root) por defecto es el actual (.), pero
también podemos especificar otro en este momento.
Una vez estamos dentro, podemos ejecutar código Lua tecleándolo directamente en la
consola y éste se enviará y ejecutará en el dispositivo conectado. Para ver una referencia
completa del API del motor en Lua, remitimos al capítulo correspondiente (Manual del API
de Lua).
Para acceder a funcionalidades extra de la consola que no pertenecen al lenguaje de script,
empezaremos a escribir la orden comenzando con el carácter | (pipe). De esta forma tenemos
las siguientes funciones:
- |quit Sale de la consola
- |transfer <nombre_fichero> Envía un archivo al dispositivo conectado.
Daniel Ponsoda Montiel
63
- |update Busca ficheros actualizados de forma recursiva en el directorio del proyecto
y los envía si su fecha de modificación es posterior a la versión que está en el dispositivo
conectado.
Todo ello se podrá hacer conectado por wifi sin necesidad de cables como tradicionalmente
enviaríamos archivos al móvil. Además, obsérvese que ya no es necesario reinstalar la
aplicación con motivo de un cambio en un asset. Los ficheros actualizados son reconocidos
automáticamente por el reproductor previamente instalado.
Daniel Ponsoda Montiel
65
6 ELECCIÓN DE LAS DEPENDENCIAS
Como último paso antes de comenzar con la implementación, en esta sección vamos a
hacer una comparativa de las distintas librerías disponibles como alternativa para cada uno
de los sistemas a integrar. Así podremos decidir con criterio cuáles serán las elegidas para
nuestro motor.
Los factores determinantes serán el tipo de licencia, las funcionalidades, la documentación
y la portabilidad a las plataformas objetivo de nuestro proyecto, que inicialmente serán PC
(Windows/Linux) y Android.
6.1 Librería de matemáticas De entre las librerías de código abierto y libres que podemos encontrar para este propósito,
sin duda la más popular es glm, la cual trata de imitar el interfaz de las funciones del lenguaje
de shader glsl de OpenGL. Esta similitud la hace ideal si usamos este API de gráficos, además
es muy sencillo encontrar documentación tanto para glm como para glsl lo cual hace sencillo
el desarrollo.
No obstante, como veremos en los siguientes apartados, los motores que vamos a usar para
gráficos y física ya incorporan sus propias librerías de matemáticas. Esto es un inconveniente
ya que cada uno utiliza la suya, con lo cual en ocasiones podremos necesitar usar alguna de
las extensiones de integración de estos motores o convertir de una estructura a otra cuando
pasemos datos entre estos dos sistemas.
En el futuro, si implementamos nuestras propias librerías de gráficos y física en el motor,
podemos utilizar una librería de matemáticas común para todo el proyecto o crear la nuestra
Game engine multiplataforma
66
propia, pero por ahora nos ceñiremos a lo que nos proporcionan los sistemas que vamos a
utilizar.
6.2 Librería de gráficos Para esta dependencia se han considerado las librerías: Ogre, Irrlicht y OpenSceneGraph
por ser las que tienen más potencial y mayor apoyo de la comunidad.
6.2.1 Ogre Se trata de un motor de gráficos 3D de código
abierto, maduro y sencillo de usar. Fue inicialmente
lanzado en 2005 bajo licencia LGPL pero, actualmente,
desde la versión 1.7 se distribuye bajo licencia MIT
desde su web (https://www.ogre3d.org/).
Recibe actualizaciones muy frecuentemente y tiene un gran apoyo por parte de la
comunidad. A fecha de 23 de junio de 2018, la última versión estable es la 1.11.1 de abril de
2018, y suele actualizarse aproximadamente cada 4 meses. Además, disponemos de la opción
de descargar el último snapshot de desarrollo del repositorio para ver el estado de las últimas
características que se actualizan a diario.
Con Ogre se han desarrollado juegos comerciales de gran éxito, como Torchlight I y II,
Hob o X-Morph: Defense, los cuales podemos comprar actualmente en plataformas como
Steam o GoG.
La documentación es muy completa, y dispone de tutoriales, una wiki oficial y una guía
de referencia, aunque a veces cuesta encontrar algo para la versión actual. Sin embargo,
habitualmente cualquier información especial que necesitemos se resuelve rápidamente en los
foros oficiales, que son muy activos (https://forums.ogre3d.org/).
Internamente funciona por medio del API Direct3D 9 y 11, OpenGL y WebGL, y en
cuanto a características técnicas dispone de todo lo que podamos necesitar para la
representación de gráficos. Entre estas funcionalidades podemos destacar:
Figura 6.1. Logo de Ogre
Daniel Ponsoda Montiel
67
Sistema cómodo de gestión de materiales (con texturas, shaders, iluminación…).
Mallas progresivas con distintos LOD generados de forma manual o automática.
Batcher estático (optimiza el renderizado).
Soporte de animación por esqueletos y arrays de pesos
Sistema de escena jerárquico basado en nodos
Distintos modos de renderizado de sombras
Sistema de partículas
Soporte para billboards, skyboxes, skyplanes y skydomes
Gestión automática de objetos transparentes
Por último, pero no menos importante, hay que mencionar que es el más portable de todos
los motores analizados, con soporte de serie para Windows, Linux, Mac, Android, iOS y
Windows Phone.
6.2.2 Irrlicht Este motor fue desarrollado por Nikolaus
Gebhardt y lanzado por primera vez en 2003. Su
licencia está basada en la zlib/libpng. Su desarrollo no
es tan activo como el de Ogre, siendo la última versión estable la 1.8.4 del 9 de julio de 2016
(hace más de dos años). No obstante, sigue siendo una opción a considerar ya que su desarrollo
sigue vivo en el repositorio de sourceforge (http://irrlicht.sourceforge.net/), y además, al ser
de código abierto, existe la posibilidad de mejorarlo o corregirlo para nuestras necesidades. De
hecho, si miramos en este repositorio podemos encontrar los avances con la versión 1.9, la cual
utiliza el API de gráficos Vulkan, dejando ya atrás OpenGL.
En cuanto a los juegos o motores creados con Irrlicht, no son tan populares como en el
caso de Ogre ni tan actuales. Entre ellos podemos destacar Bolzplatz o Gekkeiku Online.
La documentación dispone de multitud de tutoriales oficiales que demuestran toda la
funcionalidad del motor. La guía de referencia, aunque escasa (está generada automáticamente
desde el código) está bastante bien estructurada. Sin embargo, no tiene una wiki oficial como
la de Ogre.
Figura 6.2. Logo de Irrlicht
Game engine multiplataforma
68
Algo destacable de este motor es sin duda la gran cantidad de bindings (no oficiales) que
existen para multitud de lenguajes como C++, Java, Lua, Python, Perl o Ruby.
Sobre las funcionalidades que ofrece, si nos ceñimos a su última versión estable, no tiene
mucho que destacar con respecto a Ogre, pero podemos mencionar las siguientes:
Sistema de animación de agua. En Ogre también se puede hacer, e incluso de manera
más realista que la simulación que trae Irrlicht, aunque requiere de plugins externos
como el Hydrax creado por Xavier Verguín González.
Sistema de geomipmapping. Se trata de una técnica que optimiza el renderizado de
terrenos utilizando distintos niveles de detalle según la distancia.
6.2.3 OpenSceneGraph Este motor surge originalmente para aplicaciones de simulación
visual o científica en tiempo real, aunque se puede utilizar perfectamente
para videojuegos, de hecho, se le considera el más eficiente y estable de
entre los tres analizados. Este proyecto se inició en 1998 por Don Burns,
y su última versión es la 3.6.0 de Abril de 2018. Utiliza una licencia
personalizada para OpenSceneGraph basada en LGPL.
En realidad, no existe actualmente ningún videojuego o motor de juegos que utilice esta
librería gráfica. Su principal baza es que permite aprovechar la estructura de grafo para
optimizar de forma semi-automática el renderizado de los elementos de la escena ordenándolos
y agrupándolos por tipo de material, shader, textura, etc. y así evitar cambios de contexto en
el hardware.
La documentación no es muy extensa. Aunque las guías de iniciación son bastante útiles
y existen libros dedicados a este motor.
Entre las características que lo distinguen de Ogre e Irrlicht destacamos:
Utiliza un state graph, el cual permite optimizar las llamadas a OpenGL minimizando
el número de cambios de estado.
Figura 6.3. Logo de OpenSceneGraph
Daniel Ponsoda Montiel
69
Dispone de un profiler de renderizado con el que podemos detectar cuellos de botella.
Es muy estable.
Es modulable y sencillo de extender por medio de lo que denominan OSG “kits”.
6.2.4 Decisión sobre la librería de gráficos Inicialmente, la opción favorita era Ogre, pero tras probar las tres alternativas, surgen
nuevos inconvenientes. El principal de ellos es que en Ogre no hay implementado un interfaz
que nos abstraiga de ciertos detalles dependientes de la plataforma (Windows/Android) como
la gestión de los eventos de hardware, la creación de la ventana o la superficie de OpenGL.
De hecho, la última versión estable, en realidad tiene problemas sin resolver en Android
tal como se puede deducir de los foros oficiales. Por otro lado, si vamos a la versión anterior,
el soporte para Android está pensado para ser utilizado desde Java por medio de JNI, lo cual
no nos interesa porque nuestro motor estará en C++ y por tanto nos dificultaría la
reutilización de código o nos obligaría a crear adaptadores.
Además, no existe un ejemplo sencillo para Android desde el cual partir, sino que la única
aplicación completa que nos proporciona el SDK es el SampleBrowser, el cual incluye toda la
funcionalidad del motor de gráficos en un solo programa y, por tanto, es enorme, demasiado
complejo y con un diseño confuso, con lo cual resulta muy complicado desgranarlo para extraer
lo que necesitamos.
En cuanto a Irrlicht, se descarta principalmente por no disponer de una versión estable
actualizada, con lo cual el que nos queda es OpenSceneGraph que, aunque también tiene
sus inconvenientes como la falta de documentación de nivel intermedio, no nos ha dado
demasiados problemas a la hora de integrar en nuestro proyecto tanto en Windows, Linux o
Android.
6.3 Librería de física Existen pocas alternativas para física 3D tan
maduras, estables y bien documentadas como Bullet
Physics (http://bulletphysics.org/wordpress/), así que Figura 6.4. Logo de Bullet
Game engine multiplataforma
70
podríamos decir que la decisión es clara y ya está tomada desde el principio. No obstante,
listamos a continuación otras librerías que se han considerado:
MuJoCo: (http://www.mujoco.com). Viene de “Multi-Joint Dinamics with Contact”.
Es una librería de pago muy eficiente, con multitud de características y principalmente
diseñada para facilitar la investigación en robótica y biomecánica.
ODE: (http://www.ode.org/). Para simulación de dinámica de cuerpos rígidos. Se ha
utilizado en multitud de juegos comerciales como Call of Juarez, Dead Island, Mario
Strikers Charged o Resident Evil: The Umbrella Chronicles. La última versión estable
es de 2014.
Newton Dynamics: (http://newtondynamics.com). Con licencia zlib y muy
actualizada, aunque la lista de funcionalidades no llega al nivel de Bullet, y la
documentación tampoco es tan abundante.
Por su parte, Bullet Physics es un motor de física utilizado en videojuegos (por ejemplo,
Grand Theft Auto V), en el cine (con películas como Shrek 4 o Cómo Entrenar a tu Dragón),
en herramientas de diseño 3D (como Blender) o incluso en otros motores de juegos, como
Torque3D, Xenko o C4 Engine. Esto nos puede dar una idea del grado de madurez de esta
librería.
Bullet se actualiza periódicamente y su documentación es muy completa y accesible.
Además, dispone de una wiki con gran cantidad de tutoriales para todos los niveles, aunque
los más avanzados se encuentran permanentemente en construcción.
Las funcionalidades más destacables de este motor de física son las siguientes:
Simulación de cuerpos rígidos y blandos
Detección de colisiones discreta
Detección de colisiones continua y tiempo de impacto con rotación
Varias primitivas básicas y malla de colisión
Calculo de punto más cercano y estimación de profundidad de penetración
Descomposición en objetos convexos
Daniel Ponsoda Montiel
71
6.4 Lenguaje de scripting Para el propósito de poder hacer cambios en el código de los juegos sin necesidad de
recompilar e incluso editar el código en tiempo de ejecución, integraremos un motor de script
que deberá ser lo más eficiente posible, ya que tendrá que funcionar sin perjudicar el ritmo
del juego.
Para el script se han considerado los lenguajes Lua, JavaScript y Python que pasamos a
analizar a continuación.
6.4.1 Lua Aparecido en 1993, y actualmente publicado bajo licencia MIT
(https://www.lua.org/), este lenguaje imperativo basado en C se ha
usado en infinidad de aplicaciones y videojuegos comerciales, de los cuales
los más populares son World of Warcraft, Grim Fandango o Minecraft.
Su principal característica es que es sencillo y ligero y su máquina virtual
no requiere de ninguna dependencia para ser embebido en una aplicación
C/C++.
Además, es muy maduro, robusto y sobre todo rápido. De hecho, se le considera por varios
benchmarks como el lenguaje más rápido de entre los que son interpretados. Pero si aun así
necesitamos más velocidad, podemos hacer uso del compilador just-in-time (LuaJIT), aunque
no siempre podremos aprovechar esta ventaja en todas las plataformas. Por ejemplo, Apple
nos prohíbe esta característica para poder publicar en su AppStore.
Sus principales características son:
Tipado dinámico.
Todas las estructuras están basadas en lo que en Lua se conoce como tablas.
A partir de meta-tablas se pueden construir estructuras más complejas como objetos.
Los índices numéricos de los arrays comienzan por 1 (no por 0)
Funciones de orden superior.
Colector de basura.
Figura 6.5. Logo de Lua
Game engine multiplataforma
72
En cuanto a las opciones de binding con C/C++, la propia librería ofrece funciones de
acceso a la pila para hacer llamadas al código de manera manual. Pero, para simplificar las
cosas, existen multitud de librerías y herramientas dedicadas a facilitar la conexión entre
nuestro código C++ y Lua, como por ejemplo Sol2 o Swig.
6.4.2 JavaScript Surgido en 1995, y basado en el estándar ECMAScript, es un lenguaje
imperativo, muy similar a C, con ciertas funcionalidades que le dan
características de orientación a objetos. La principal ventaja de este lenguaje
es su popularidad, y es que prácticamente cualquier programador conoce
este lenguaje (sobre todo quien alguna vez ha programado una web), con lo
cual, si lo incorporásemos a nuestro sistema, sería sencillo encontrar
desarrolladores interesados en hacer juegos para nuestro motor.
En cuanto a su uso en videojuegos, JavaScript se utiliza principalmente en juegos web
(HTML5), e incluso es una de las opciones de script del conocido motor Unity3D.
Existen multitud de máquinas JavaScript procedentes principalmente de los navegadores,
como son SpiderMonkey (Firefox), V8 (Chrome), JavaScriptCore (Safari) o Chakra (Microsoft
Edge).
Es posible extraer el motor JavaScript a partir de los fuentes publicados de uno de estos
navegadores para embeber código en nuestro proyecto (aunque en algunos casos no es sencillo
compilarlos para Android). Sin embargo, al estar destinados a usarse para la navegación, esto
tiene como desventaja de la cantidad de carga que aporta a la máquina virtual todas las
funcionalidades de acceso al DOM y la web. Esto ralentiza innecesariamente el motor ya que
no vamos a necesitar todas estas características.
Como alternativa, existen otros intérpretes de JavaScript para C++ destinados a
microcontroladores y, por tanto, más ligeros y posiblemente suficientes para nuestro propósito,
Figura 6.6. Logo de JavaScript
Daniel Ponsoda Montiel
73
como son Tiny-JS, Espruino, V7 o JerryScript, aunque son tan minimalistas que no permiten
compilación JIT.
6.4.3 Python Este lenguaje hizo su aparición en 1991. Es multiparadigma:
orientado a objetos, imperativo y funcional. Se caracteriza por ser un
lenguaje donde prima la limpieza y organización del código. Por
ejemplo, el uso correcto de la indentación es obligatoria ya que es la
que define los bloques de código al compilar.
A pesar de no ser tan popular como JavaScript, se utiliza en una
gran cantidad de aplicaciones de ámbito científico, de cálculo,
educativo, de diseño gráfico y de desarrollo. Por ejemplo, es el script
de programación de extensiones de Blender.
La máquina Python puede compilarse e integrarse de forma sencilla con nuestro código
C++. Además, la librería está muy bien documentada. Sin embargo, en comparación con los
otros lenguajes que hemos considerado (Lua y JavaScript) su intérprete es el menos eficiente,
y es éste el principal motivo por el que queda prácticamente descartado para el desarrollo de
nuestro motor donde necesitamos exprimir en tiempo real toda la potencia posible del sistema.
6.4.4 Decisión sobre el lenguaje de scripting Finalmente escogemos Lua por ser el más ligero y eficiente. Como sistema de binding con
C++ usaremos Sol2 (https://github.com/ThePhD/sol2).
JavaScript queda descartado por ser más pesado con funcionalidades innecesarias y porque
los mejores motores JS necesitan de ciertas adaptaciones para poder compilarse en Android.
En cuanto a Python, dado que la eficiencia es un aspecto crítico para un motor de juegos,
se descarta por no ser tan eficiente como las otras alternativas analizadas.
Figura 6.7. Logo de Python
Game engine multiplataforma
74
6.5 Interfaz gráfica de usuario para el editor Puesto que nuestro motor estará escrito en C++, lo más adecuado sería que el sistema de
GUI para el editor estuviera también escrito en C++ o al menos que fuera fácil de integrar
con código escrito en este lenguaje. Además, queremos que pueda compilarse sin problemas en
varias plataformas (al menos para Windows y Linux, y en un futuro para Mac).
6.5.1 Qt Una librería que ofrece todas estas características es Qt
(https://www.qt.io), la cual tiene un gran soporte de la comunidad
y es un proyecto muy activo, respaldado entre otras compañías por
Nokia. Además, dispone de una serie de facilidades que nos vienen
muy bien dada la naturaleza de nuestro proyecto, por ejemplo:
Es posible integrar OpenSceneGraph directamente con Qt
Tiene un tipo de cuadro de texto con resaltado de sintaxis, ideal para nuestro editor
de código.
Dispone de controles avanzados y muy personalizables para cualquier propósito.
Editor de interfaces visual muy sencillo de usar.
Portable a infinidad de plataformas.
Fácil integración con C++.
Opción de licencia LGPL.
Internamente utiliza OpenGL para el renderizado de controles, lo que permite crear
interfaces avanzados y vistosos que se desmarcan de las típicas librerías del sistema. Además,
su estructura está orientada a un diseño responsive.
Una desventaja es tal vez que Qt ofrece dos licencias: Una comercial (de pago) y otra open
source gratuita (bajo LGPL / GPL). La versión comercial contiene una serie de herramientas
no disponibles en la versión libre. Además, ciertas funcionalidades están condicionadas a su
uso sólo bajo GPL.
Figura 6.8. Logo de Qt
Daniel Ponsoda Montiel
75
6.5.2 wxWidgets Se trata de otra librería de interfaz gráfica ampliamente
utilizada y disponible para su descarga desde la web oficial
https://www.wxwidgets.org. La principal característica de esta
librería es que está diseñada como un interfaz que sirve de
wrapper para la implementación interna que se usa realmente.
Dicha implementación es básicamente un port del core de la
librería y puede estar implementado bajo distintos sistemas en función de la plataforma
objetivo. Actualmente existen los siguientes ports en desarrollo activo:
wxGTK: Port recomendado para Linux
wxMSW: Port para Windows (XP, Vista, 7, 8 y 10)
wxOSX/Carbon: Para aplicaciones Carbon en Mac OS X 10.5 o superior
wxOSX/Cocoa: Para aplicaciones Cocoa en Mac OS X 10.5 o superior
wxX11: Port para Linux por medio del display X11
WxWidgets dispone de todos los controles que podamos imaginar, los cuales, como
podemos suponer, utiliza en cada plataforma la librería que le ofrece el sistema, así se asegura
el máximo de compatibilidad y evita muchos problemas. El inconveniente es que cada port
debe ser mantenido y actualizado de manera independiente y no todos estos proyectos siguen
el mismo ritmo de actualizaciones.
Un inconveniente es que el desarrollador de wxWidgets no ofrece una herramienta de
edición de ventanas visual, así que debemos recurrir a proyectos de terceros.
6.5.3 Decisión sobre la librería de GUI Por su gran soporte de la comunidad, su facilidad de uso, el editor de interfaz, su variedad
de controles como el editor de código con resaltado de sintaxis, y la posibilidad de integración
con OSG, escogemos Qt como librería de interfaz gráfica de usuario descartando wxWidgets.
Figura 6.9. Logo de wxWidgets
Game engine multiplataforma
76
6.6 Librería de sockets Una de las principales bazas de nuestro motor es que es capaz de enviar un script desde el
PC de desarrollo hasta el dispositivo de pruebas (habitualmente Android). Para ello es
necesario establecer una comunicación de red entre las dos máquinas que no necesariamente
tienen el mismo hardware o sistema operativo.
La primera idea fue la de utilizar las librerías de sockets nativas disponibles en cada
plataforma donde se implementaría el motor. Por ejemplo, para Android utilizaríamos los
sockets estándar de Unix, y para Windows elegiríamos WinSockets, por su similitud con los
primeros.
La desventaja de esto es que implica trabajar con estas librerías a bajo nivel y además,
por más que se parezcan, se hace inevitable escribir algunas partes del código específicas para
cada plataforma.
Como solución se propone utilizar Boost:Asio, la cual es una librería que proporciona,
además de multitud de funcionalidades de entrada y salida, un interfaz de alto nivel para
creación y uso de sockets muy sencillo de utilizar y multiplataforma.
El problema que surgió a partir de esta decisión es que esta librería tiene algunas
dependencias con Boost que deben ser compiladas y en este caso daba errores al tratar de
compilar para Android. Además, incrementaba innecesariamente el tamaño y tiempo de
compilación del código.
La solución final ha sido utilizar una versión de Asio directa de sus creadores que no
requiere Boost para funcionar. Podemos encontrar esta librería en la web oficial: https://think-
async.com/Asio/.
Daniel Ponsoda Montiel
77
7 CONFIGURACIÓN DEL ENTORNO
El proyecto estará programado en C++. El IDE principal será Visual Studio Community
2017 (versión 15.6.4) funcionando bajo Windows 10, pero en cualquier caso lo que haremos
será un proyecto CMake multiplataforma que podrá compilarse desde cualquier SO y podrá
usar el generador adecuado para cualquiera de nuestras plataformas objetivo. Se usará la
versión 3.11.4 de CMake.
Para la creación del interfaz de usuario del editor utilizaremos el IDE Qt Creator 4.6.2
Community, y para el port de Android del reproductor utilizaremos el IDE Android Studio
versión 3.1.3.
Para el port de Linux, dado que esta no será la plataforma principal de desarrollo, no se
utilizará ningún IDE concreto, sino que se compilará todo utilizando la herramienta make de
línea de comandos desde Ubuntu 16.04.
En los siguientes apartados se explicará cómo se han compilado todas las dependencias del
proyecto para las plataformas objetivo (Windows, Linux y Android) para finalmente crear el
proyecto inicial tanto para el editor como para el reproductor.
A continuación, se muestra la relación de todo el software utilizado junto con sus
correspondientes versiones.
Sistema operativo Versión
Windows 10 Pro 64 bits
Ubuntu 16.04 64 bits
Android 5.0.1
Game engine multiplataforma
78
IDE Versión
Visual Studio Community 2017 15.6.4
Qt Creator 4.6.2
Android Studio 3.1.3
Librería Versión
OpenSceneGraph 3.6.2
Bullet 2.87
Lua 5.3.5
Sol 2.20
Herramientas desarrollo Linux Versión
gcc 5.4.0
Make 4.1
CMake + CMake-GUI 3.5.1
Otras herramientas Versión
CMake + CMake-GUI (Windows) 3.11.4
Android SDK API Level 28
Android NDK r15c
Siguiendo las instrucciones que se describen, el proyecto creado debería funcionar en
cualquier PC con Windows o Linux, o en cualquier smartphone con Android. No obstante, se
describe a continuación el hardware utilizado durante el desarrollo para así garantizar la
reproductividad de los pasos.
Hardware de desarrollo (escritorio)
Procesador Intel Core i7 2600K
Arquitectura 64 bits
Número de núcleos 4
Velocidad de reloj 3800 Mhz
Daniel Ponsoda Montiel
79
Memoria RAM 2x4Gb DDR3
GPU NVidia GeForce GTX 750
Memoria gráfica 1Gb GDDR5
Hardware de desarrollo (móvil)
Terminal Samsung Galaxy S4 I9505
Procesador Krait 300
Número de núcleos 4
Velocidad de reloj 1900 Mhz
Memoria RAM 2Gb
GPU Qualcomm Adreno 320
Notas generales
- Todos los fuentes y proyectos se crearán en carpetas cuyas rutas no tengan espacios
ni caracteres especiales.
- Al usar CMake, siempre estableceremos el directorio de instalación a una carpeta para
la que no se necesiten permisos de administración para escribir en ella. Por defecto
está en C:/Program Files/<nombre_paquete>”, y por tanto la cambiaremos.
- Todos los archivos con los que trabajaremos para nuestro proyecto partirán de la ruta
d:/tfg que, para generalizar las explicaciones denominaremos <ruta_tfg>.
- Al compilar las dependencies con Visual Studio lo haremos para x64-Release y x64-
Debug, para poder usarlas en nuestro proyecto de manera indistinta.
Notas para Linux
Necesitaremos tener el entorno preparado para desarrollo en C++. Además, el proyecto
utiliza aceleración gráfica por hardware. Para ello es necesario instalar algunos paquetes con
las ordenes:
sudo apt-get install build-essential sudo apt-get install libfontconfig1 sudo apt-get install mesa-common-dev sudo apt-get install libglu1-mesa-dev -y
Para asegurarnos de que tenemos OpenGL configurado correctamente, escribimos:
Game engine multiplataforma
80
glxinfo | grep "OpenGL version"
Esto debería mostrar una versión de OpenGL 3.0 o superior. Si no fuera así, puede que
tengamos un hardware insuficiente, o bien si estamos en una máquina virtual, puede que no
hayamos activado la aceleración de gráficos por hardware.
Un problema que puede ocurrir si usamos VirtualBox es que los drivers de GPU que instala
con el paquete de “Guest Additions” no funcionan correctamente en Ubuntu, y es mejor usar
los que trae la instalación del SO por defecto.
7.1 Preparar OpenSceneGraph Descargamos los fuentes de OpenSceneGraph desde la release de github correspondiente a
la versión que vamos a usar (3.6.2):
https://github.com/openscenegraph/OpenSceneGraph/releases/tag/OpenSceneGraph-3.6.2
Descargamos también la versión simplificada del paquete de dependencias:
https://download.osgvisual.org/3rdParty_VS2017_v141_x64_V11_small.7z
Ahora descomprimimos el contenido de estos paquetes en la siguiente estructura de
carpetas:
<ruta_tfg>/deps/osg-src
3rdParty Dependencias de OpenSceneGraph
OpenSceneGraph Fuentes de OpenSceneGraph
7.1.1 Compilar para Windows Abrimos CMake GUI y en el campo “Where is the source code” escribimos
<ruta_tfg>/deps/osg-src/OpenSceneGraph.
En el campo “Where to build de binaries” ponemos <ruta_tfg>/deps/osg-src/build (crear
la carpeta si es necesario).
Daniel Ponsoda Montiel
81
Pulsamos “Configure” y en la ventana que aparecerá, seleccioamos el generador Visual
Studio 15 2017 Win64, marcamos “Use default native compileres” y pulsamos “Finish”.
Nos aseguramos de que los siguientes parámetros están así:
ACTUAL_3RDPARTY_DIR <ruta_tfg>/deps/osg-src/3rdparty
CMAKE_INSTALL_PREFIX <ruta_tfg>/deps/osg-sdk
Dejamos el resto de opciones como están y pulsamos “Configure”. Si no ha habido
problemas se pondrán todas en blanco. Pulsamos “Generate” para crear el proyecto de Visual
Studio en la carpeta <ruta_tfg>/deps/osg-src/build.
Hecho esto abrimos con Visual Studio la solución generada (archivo OpenSceneGraph.sln)
y, seleccionando el target x64 (Debug) compilamos el proyecto “BUILD_ALL”. Tardará
varios minutos.
Si no ha habido ningún problema, compilamos “INSTALL” para mover las librerias,
binarios y cabeceras a la carpeta de instalación que definimos en CMake
(<ruta_tfg>/deps/osg-sdk).
Ahora definimos/modificamos las siguientes variables de entorno de Windows:
OSG_ROOT <ruta_tfg>/deps/osg-sdk
PATH Añadir: %OSG_ROOT%/bin y %OSG_ROOT%/3rdParty/bin
La librería OpenSceneGraph ya está lista para usarse en nuestro proyecto Windows.
7.1.2 Compilar para Linux Seguimos los mismos pasos realizados para Windows con CMake-GUI pero con el target
Unix Makefiles. Para Linux sí que usaremos el directorio de instalación establecido por defecto
(/usr/lib/)
Por otro lado, al contrario de la versión Windows, para Linux no existen binarios
compilados de las dependencias, de manera que, si queremos aprovechar todas las
características que nos ofrece OSG, tendremos que instalar previamente estas dependencias
Game engine multiplataforma
82
antes de configurar la compilación con CMake. En cualquier caso, afortunadamente, para el
uso que por el momento daremos de la librería, no necesitaremos más dependencias de las que
nos ofrece el paquete build-essentials de Linux (Lo instalaremos si no lo tenemos).
Cuando tengamos el proyecto generado, lo compilamos desde el terminal accediendo al
directorio <ruta_tfg>/deps/osg-src/build y escribiendo la orden:
make -j 4 make install
Por último, añadiremos las mismas variables de entorno que hemos establecido en windows
con las ordenes:
export OSG_ROOT=<ruta_tfg>/deps/osg-sdk export PATH=$PATH:$OSG_ROOT/bin
7.1.3 Compilar para Android Gracias a CMake, la compilación del SDK es fácil de realizar en ambas plataformas. No
obstante, para la versión de Android se han seguido unos pasos especiales.
En primer lugar, no compilaremos en Windows, sino que, excepcionalmente, usaremos
Ubuntu para compilar los fuentes dado que es más sencillo configurar el entorno que
necesitamos para tal fin.
Se ha utilizado Ubuntu 16.04, con CMake 3.5.1. Los pasos a seguir han sido los siguientes:
En primer lugar, si no tenemos el Android NDK, lo descargamos (debe ser la versión r15c),
lo descomprimimos en la carpeta ~/ndk y lo añadimos al PATH con la orden:
export PATH=~/ndk:$PATH export ANDROID_NDK=~/ndk
Descargamos y descomprimimos los fuentes de nuestra versión de OSG en ~/osg-src y
creamos una carpeta paralela a ésta para el destino de la instalación a la que llamamos ~/osg-
sdk.
Ahora entramos en osg-src y escribimos:
mkdir build_android_static_gles2 && cd build_android_static_gles2 cmake .. -DANDROID_NDK=~/ndk \
Daniel Ponsoda Montiel
83
-DCMAKE_TOOLCHAIN_FILE=../PlatformSpecifics/Android/android.toolchain.cmake \ -DOPENGL_PROFILE="GLES2" -DDYNAMIC_OPENTHREADS=OFF -DDYNAMIC_OPENSCENEGRAPH=OFF \ -DANDROID_NATIVE_API_LEVEL=15 \ -DANDROID_ABI=armeabi-v7a \ -DCMAKE_INSTALL_PREFIX=<ruta-al-directorio-de-instalación> make -j 8 make install
La <ruta-al-directorio-de-instalación> deberemos indicarla de forma absoluta (y tampoco
sirve usar el carácter ~, ya que nos dará un error al instalar). Si no ha habido problemas,
tendremos el sdk listo para usar en el directorio de instalación. Ahora nos lo podemos llevar
para usarlo desde Windows con Android Studio.
7.2 Preparar Bullet Physics Descargamos Bullet 2.87 desde la página de releases del repositorio de Github:
https://github.com/bulletphysics/bullet3/releases
Descomprimimos los fuentes en <ruta_tfg>/deps/bullet-src y creamos una carpeta
<ruta_tfg>/deps/bullet-src/build
7.2.1 Compilar para Windows Esta versión de Bullet viene con los archivos CMake para poder preparar el proyecto para
Visual Studio en Windows.
Abrimos CMake-GUI y establecemos los directorios de fuente y build que hemos creado.
Pulsamos configurar, y seleccionamos Visual Studio 15 2017 Win64.
Cambiaremos varias opciones en la configuración. Lo que haremos es evitar que se
compilen las demos y funcionalidades extra de la librería. Además, como no vamos a necesitar
el entorno gráfico de depuración, evitamos también que Bullet dependa de GLUT y OpenGL.
Por último, activando MSVC_RUNTIME_LIBRARY_DLL, hacemos que la librería utilice
el runtime dinámico, ya que de lo contrario no podría combinarse con OpenSceneGraph que
también está configurado así.
Game engine multiplataforma
84
Las opciones que cambian son:
BUILD_BULLET2_DEMOS OFF
BUILD_BULLET3 OFF
BUILD_CPU_DEMOS OFF
BUILD_EXTRAS OFF
BUILD_OPENGL3_DEMOS OFF
CMAKE_INSTALL_PREFIX <ruta_tfg>/deps/bullet-sdk
INSTALL_LIBS ON
USE_GLUT OFF
USE_GRAPHICAL_BENCHMARK OFF
USE_MSVC_RUNTIME_LIBRARY_DLL ON
Cuando pulsemos otra vez “Configure” nos aparecerán nuevas opciones en rojo para
completar. Lo haremos así:
INCLUDE INSTALL_DIR <ruta_tfg>/deps/bullet-sdk/include
LIB_DESTINATION <ruta_tfg>/deps/bullet-sdk/lib
PKGCONFIG_INSTALL_PREFIX <ruta_tfg>/deps/bullet-sdk/lib/pkgconfig
Configuramos y generamos y ya tendremos el proyecto de Visual Studio en
<ruta_tfg>/deps/bullet-src/build
Tras compilar BUILD_ALL e INSTALL con Visual Studio (en Release y Debug), como
último paso, tendremos que editar el archivo:
<ruta_tfg>/deps/bulletsdk/lib/cmake/bullet/BulletConfig.cmake
y cambiar la siguiente línea:
set ( BULLET_LIBRARIES "LinearMath;Bullet3Common;BulletInverseDynamics;\ BulletCollision;BulletDynamics;BulletSoftBody" )
Por:
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
Daniel Ponsoda Montiel
85
set ( BULLET_LIBRARIES "LinearMath_Debug;Bullet3Common_Debug;BulletInverseDynamics_Debug;\ BulletCollision_Debug;BulletDynamics_Debug;BulletSoftBody_Debug" )
else() set ( BULLET_LIBRARIES
"LinearMath;Bullet3Common;BulletInverseDynamics;\ BulletCollision;BulletDynamics;BulletSoftBody" )
endif()
Hecho esto podremos usar indistintamente una compilación Debug o Release en nuestro
proyecto. Con esto, Bullet ya estaría listo para usar.
7.2.2 Compilar para Linux Seguimos los mismos pasos realizados para Windows con CMake-GUI pero con el target
Unix Makefiles. Al igual que con OSG, en Linux dejaremos la ruta de instalación por defecto
(/usr/lib/)
Cuando tengamos el proyecto generado, lo compilamos e instalamos desde el terminal
accediendo al directorio <ruta_tfg>/deps/bullet-src/build y escribiendo las ordenes:
make -j 4 make install
Esto nos dejará la librería lista para usar en el sistema.
7.2.3 Compilar para Android Para Android, volveremos de nuevo a Ubuntu asegurándonos de que tenemos instalado
Android NDK r15c con las variables de entorno definidas:
export PATH=~/ndk:$PATH export ANDROID_NDK=~/ndk
Descargamos y descomprimimos Bullet en cualquier carpeta y, desde la línea de comandos,
entramos en <ruta_bullet>/build3/Android/jni y ejecutamos:
ndk-build
Esto compilará la librería estática libbullet.a que podremos encontrar en:
<ruta_bullet>/build3/Android/obj/local/armeabi-v7a
Al igual que con OSG, nos la llevamos a Windows para usarla en el proyecto Android
Studio que crearemos más adelante.
Game engine multiplataforma
86
7.3 Preparar Lua Utilizaremos la versión 5.3.5 de Lua junto con el binding para C++ Sol2 versión 2.20.
Podemos descargar Lua desde:
https://www.lua.org/ftp/lua-5.3.5.tar.gz
Descomprimimos los fuentes en <ruta_tfg>/deps/lua-src
Por otro lado, Sol2 es una librería sólo de cabeceras y por tanto no requiere compilarse.
Podemos descargar los archivos sol.hpp y sol_forward.hpp listos para usar desde:
https://github.com/ThePhD/sol2/releases
7.3.1 Compilar para Windows En la versión Windows de los fuentes, esta vez no hay un CMakeLists.txt que nos facilite
el trabajo pero, por suerte, la librería Lua es auto-contenida y muy sencilla de compilar, así
que creamos un proyecto vacío de C++ con Visual Studio para este cometido.
Lo primero que hacemos es agregar todos los archivos de fuentes de Lua al proyecto (todos
excepto lua.c y luac.c). Ahora, vamos a Propiedades > General > Tipo de configuración, e
indicamos “Biblioteca estática (lib)”.
Por último, compilamos el proyecto con la misma arquitectura usada para las otras
librerías (x64 debug). Podremos encontrar el fichero lua.lib en la carpeta x64/Debug del
proyecto.
Por comodidad, movemos este fichero junto con los archivos .h y hpp a una carpeta
dedicada a esta librería. En nuestro caso, <ruta_tfg>/deps/lua-sdk
Opcionalmente, para que al usar CMake sea más sencillo encontrar la librería, podemos
crear el archivo LuaConfig.cmake en <ruta_tfg>/deps/lua-sdk/lib/Lua/cmake con el
siguiente código:
set ( LUA_FOUND 1 ) set ( LUA_USE_FILE "<ruta_tfg>/deps/lua-sdk/lib/Lua/cmake/UseLua.cmake" ) set ( LUA_DEFINITIONS "" )
Daniel Ponsoda Montiel
87
set ( LUA_INCLUDE_DIR "<ruta_tfg>/deps/lua-sdk/include" ) set ( LUA_INCLUDE_DIRS "<ruta_tfg>/deps/lua-sdk/include/lua; <ruta_tfg>/deps/lua-sdk/include/sol" ) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set ( LUA_LIBRARIES "lua-debug" ) else() set ( LUA_LIBRARIES "lua" ) endif() set ( LUA_LIBRARY_DIRS "<ruta_tfg>/deps/lua-sdk/lib" ) set ( LUA_ROOT_DIR "<ruta_tfg>/deps/lua-sdk" ) set ( LUA_VERSION_STRING "2.87" )
Y otro para usar la librería en la misma carpeta con el nombre UseLua.cmake y el código:
add_definitions ( ${LUA_DEFINITIONS} ) include_directories ( ${LUA_INCLUDE_DIRS} ) link_directories ( ${LUA_LIBRARY_DIRS} )
7.3.2 Compilar para Linux La compilación en Linux se hace de forma directa con la orden make gracias al fichero
Makefile. Concretamente, ejecutaremos:
cd <ruta_fuentes_lua> make generic make install
7.3.3 Compilar para Android El desarrollador de Lua no nos proporciona un Android.mk ni un archivo CMake para
facilitarnos el trabajo, pero podemos compilar Lua fácilmente de manera similar a como lo
hemos hecho para Windows.
En este caso podemos añadir directamente los fuentes a un proyecto de Android Studio
con soporte para C++ copiando los ficheros a la carpeta src/main/cpp/lua y declarándolos
en el CMakeLists.txt que genera el IDE de esta forma:
add_library(lua STATIC src/main/cpp/lua/lapi.c src/main/cpp/lua/lapi.h src/main/cpp/lua/lauxlib.c src/main/cpp/lua/lauxlib.h # ... (resto de fuentes de lua)
Luego lo enlazamos con la librería del proyecto:
target_link_libraries(native-lib lua # ... (resto de librerías del proyecto)
Game engine multiplataforma
88
Ahora generamos el proyecto con Build > Make Project y se generará un archivo liblua.a
en <carpeta_del_proyecto>/app/.externalNativeBuild/cmake/debug/armeabi-v7a, o en la
carpeta correspondiente a la arquitectura configurada en el fichero gradle (ver más detalles en
la sección de Proyecto de inicio).
7.4 Preparar el framework Qt Utilizaremos Qt versión 5.11.1 con licencia LGPL que podemos descargar desde la web
oficial:
https://www.qt.io/download-qt-installer
Lo que obtenemos según la plataforma donde nos encontremos, es un instalador que nos
facilitará el trabajo para configurar Qt. En este caso se ofrecen directamente los binarios y,
por tanto, no es necesario compilar nada. Durante el proceso de instalación se nos preguntará
qué componentes queremos instalar. Marcaremos los siguientes:
Qt 5.11.1
- MSVS 2017 64 bits
- + Todos los componentes que empiezan por Qt *
Tools
- Qt Creator
Tras finalizar la instalación, si todo ha ido bien, tendremos Qt listo para usar en la carpeta
de destino que hayamos escogido.
7.5 Preparar Asio Esta librería puede usarse en forma de archivos de cabecera (.h) por lo que no requiere
compilación. Además, funciona directamente sin problemas en las plataformas para las que
está escrito el motor (Windows, Linux y Android).
Sin embargo, curiosamente el enlace que se proporciona en el apartado de descargas de
esta la web oficial (https://think-async.com/Asio/) apunta a una versión que se encuentra
desactualizada y que depende de Boost (lo cual queríamos evitar).
Daniel Ponsoda Montiel
89
Para adquirir la última versión lo haremos directamente desde el repositorio Git:
https://github.com/chriskohlhoff/asio/ En nuestro caso utilizaremos la versión 1.12.2.
7.6 Proyecto del reproductor para Windows/Linux En este apartado se explicará cómo configurar nuestro proyecto para trabajar con las
dependencias que hemos preparado en los apartados anteriores. En primer lugar, haremos el
proyecto para Windows, pero, aunque vamos a usar Visual Studio, crearemos un proyecto
CMake para facilitar la portabilidad.
Una vez creado, abrimos el archivo CMakeLists.txt del proyecto (no el de la solución) y
añadimos las dependencias. Definiremos las rutas a las librerías en las siguientes variables.
Son éstas las que esperan los módulos de find_package:
set(OSG_ROOT <ruta_tfg>/deps/osg-sdk) set(BULLET_ROOT <ruta_tfg>/deps/bullet-sdk) set(LUA_ROOT <ruta_tfg>/deps/lua-sdk)
Ahora buscamos las librerías:
find_package(OpenSceneGraph REQUIRED COMPONENTS osgDB osgGA osgUtil osgViewer) find_package(Bullet REQUIRED) find_package(Lua CONFIG REQUIRED HINTS ${LUA_ROOT})
Y añadimos los directorios de las cabeceras y librerías:
include_directories(${OPENSCENEGRAPH_INCLUDE_DIRS} ${BULLET_INCLUDE_DIR}
${LUA_INCLUDE_DIR}) link_directories(${OPENSCENEGRAPH_LIBRARY_DIRS} ${BULLET_INCLUDE_DIR}
${LUA_LIBRARY_DIRS})
Declaramos el ejecutable:
add_executable (player Main.cpp)
Y enlazamos las librerías:
target_link_libraries(player ${OPENSCENEGRAPH_LIBRARIES} ${BULLET_LIBRARIES}
Game engine multiplataforma
90
${LUA_LIBRARIES})
Este archivo CMake debería funcionar para compilar la versión de Windows con Visual
Studio y para la versión Linux en Ubuntu con CMake y make.
7.7 Proyecto del reproductor para Android Para el proyecto Android nos basamos en el app de ejemplo del SDK de OpenSceneGraph:
https://github.com/openscenegraph/OpenSceneGraph/tree/master/examples/osgAndroidEx
ampleGLES2
Lo usaremos con Android Studio utilizando la opción de importar proyecto.
El archivo CMakeLists.txt para Android estará basado en el que nos proporcina Android
Studio en el ejemplo de NativeActivity.
Al añadir las dependencias, en el entorno de desarrollo de Android no nos funcionarán
módulos find_package, con lo que tendremos que indicar las rutas manualmente para los
archivos de cabecera y las librerías de la siguiente forma:
include_directories(<ruta_tfg>/deps/osg-sdk-android/include <ruta_tfg>/deps/bullet-sdk-android/include <ruta_tfg>/deps/bullet-sdk-android/include/bullet <ruta_tfg>/deps/lua-sdk-android/include/lua <ruta_tfg>/deps/lua-sdk-android/include/sol) link_directories(<ruta_tfg>/deps/osg-sdk-android/lib <ruta_tfg>/deps/osg-sdk-android/lib/osgPlugins-3.7.0 <ruta_tfg>/deps/bullet-sdk-android/lib <ruta_tfg>/deps/lua-sdk-android/lib)
Luego, para enlazar con la librería nativa de Android, lo haremos indicando las librerías
necesarias una a una:
target_link_libraries(osgNativeLib EGL GLESv2osgViewer osgGA osgDB osgUtil osg OpenThreads z
Bullet lua)
7.8 Proyecto de la consola para Windows/Linux La consola es una aplicación de línea de comandos que nos permitirá enviar código en línea
y archivos desde el PC de desarrollo al reproductor que tengamos conectado en nuestra red.
Daniel Ponsoda Montiel
91
Esta vez el archivo CMakeLists.txt es muy sencillo. A continuación mostramos lo
fundamental:
set(PLAYER_ROOT d:/tfg/player/player) set(ASIO_ROOT d:/tfg/deps/asio) set(DIRENT_ROOT d:/tfg/deps/dirent) include_directories(${ASIO_ROOT}/include ${PLAYER_ROOT}) include_directories(${DIRENT_ROOT}/include ${DIRENT_ROOT}) file(GLOB ion_common ${PLAYER_ROOT}/common/fileUtil.h ${PLAYER_ROOT}/common/fileUtil.cpp) add_executable (console "console.cpp" ${ion_common})
Como vemos, vamos a reutilizar código presente en el player. En este caso simplemente
necesitamos las utilidades de acceso a fichero de common/fileUtil. Además, como dependencias
externas, necesitaremos Asio (para acceso a red) tanto en Windows como Linux.
En el caso de Windows utilizaremos una implementación no oficial de dirent para Windows
que necesitamos para explorar los archivos del directorio de trabajo utilizando el mismo API
de dirent para Linux. Este port ha sido creado por Tony Rönkkö, y se encuentra disponible
en el siguiente repositorio de GitHub: https://github.com/tronkko/dirent
7.9 Proyecto para el editor gráfico El interfaz del editor lo desarrollaremos usando Qt Creator. En nuestro caso crearemos un
nuevo proyecto “Qt Widgets Application” utilizando el kit de desarrollo de Visual Studio
(MSVC2017).
Una vez creado, para añadir las librerías, hacemos click derecho sobre el proyecto y
pulsamos “Add library…”. Añadiremos por ejemplo la de OSG indicando sus rutas y, al
hacerlo, se creará un archivo de configuración del proyecto (.pro) que podremos editar.
Modificaremos las siguientes líneas para añadir los componentes restantes de OSG:
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../deps/osg-sdk/lib/ -losgDB -losgGA -losgUtil -losgViewer -losg else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../deps/osg-sdk/lib/ -losgDBd -losgGAd -losgUtild -losgViewerd -losgd INCLUDEPATH += $$PWD/../../deps/osg-sdk/include DEPENDPATH += $$PWD/../../deps/osg-sdk/include
Game engine multiplataforma
92
IMPORTANTE: Siempre que modifiquemos la configuración del proyecto tendremos
que llamar a qmake desde Build -> Run qmake para que se apliquen los cambios.
A continuación, añadimos de la misma forma las dependencias para Bullet y Lua, y ya
tenemos el proyecto listo para empezar.
7.9.1 Integración de Qt con OpenSceneGraph (osgQt) Para poder mostrar una vista de OpenSceneGraph en una ventana Qt, en primer lugar
tendremos que crear un widget personalizado que heredará del QOpenGLWidget de Qt.
Para un funcionamiento básico, en el constructor inicializaremos los objetos osgView y
osgGraphicsWindowEmbedded de OSG, y sobreescribiremos el método virtual paintGL para
que actualice la vista tal como se muestra en el siguiente código:
class QOSGWidget : public QOpenGLWidget { public: QOSGWidget(QWidget* parent = 0) : QOpenGLWidget(parent) { graphicsWindow = new osgViewer::GraphicsWindowEmbedded( 0,0,this->width(),this->height()); viewer = new osgViewer::Viewer(); osg::Camera* camera = viewer->getCamera(); camera->setGraphicsContext( graphicsWindow ); // Escribimos a continuación el código de inicialización de la escena OSG // tal como lo hacemos para el player (añadimos objetos, luces, etc.) // ... } protected: virtual void paintGL() { viewer->frame(); } virtual bool event(QEvent* event){ bool handled = QOpenGLWidget::event(event); this->update(); return handled; } private: osg::ref_ptr<osgViewer::GraphicsWindowEmbedded> graphicsWindow; osg::ref_ptr<osgViewer::Viewer> viewer; osg::ref_ptr<osg::Group> root; };
Ahora, desde el método main del programa, inicializamos la ventana y le añadimos este
widget para que muestre los gráficos OSG:
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow window; QOSGWidget* widget = new QOSGWidget();
Daniel Ponsoda Montiel
93
window.setCentralWidget(widget); window.show(); return a.exec(); }
Podemos encontrar más información sobre este método de integración de OSG con Qt en
el código de ejemplo que proporciona Victoria Rudakova en Github:
https://github.com/vicrucann/QtOSG-hello/blob/master/main.cpp
Daniel Ponsoda Montiel
95
8 DISEÑO DEL CÓDIGO
En este apartado se van a exponer los distintos diagramas UML de clases tal como
finalmente quedaron tras adoptar todas las medidas necesarias para que todo encajara. No se
trata del diseño inicial del modelo de dominio.
Los diagramas están ordenados alfabéticamente por sistemas y contienen una breve
descripción con sus características principales. Para más detalles acerca del porqué de algunas
decisiones de diseño, y de cambios sobre el modelo original, nos remitiremos al siguiente
capítulo (Implementación) donde se hace un análisis más exhaustivo sobre las cuestiones más
relevantes.
8.1 Sistema de administración de assets
Figura 8.1. Diagrama de clases. Sistema de administración de assets
Game engine multiplataforma
96
El sistema de administración de assets se encarga de gestionar los objetos obtenidos a
partir de ficheros de disco. Sus características son:
Permite mapear los recursos por nombre de fichero
Evita cargar varias veces ficheros a los que ya se ha accedido (a menos que
explícitamente se solicite recargar).
Permite tener múltiples tipos de cargadores para un mismo tipo de asset (por ejemplo,
para texturas, podríamos tener cargadores png, jpg, gif…)
Detecta y elimina del caché recursos que ya no se usan en la escena
Devuelve el tipo de objeto adecuado de forma segura
8.2 Sistema de configuración
Figura 8.2. Diagrama de clases. Sistema de configuración
El sistema de configuración consta de una única clase. Al instanciarla pasamos el nombre
del archivo de configuración al constructor. Este archivo no es más que un fichero de texto
que contiene los pares atributo-valor que conforman la configuración que utilizaremos. No se
trata de una clase de instancia única (Singleton). De hecho, puede utilizarse para varios
propósitos. Por ejemplo, además de la configuración del motor, puede emplearse para el
archivo time.txt del sistema de sincronización de reloj, y también puede ser usado por el
programador usuario del motor para guardar información, opciones o configuración del juego
que está realizando. Las características son:
Permite autoguardado en disco de las opciones al ser establecidas o bien hacer un
guardado manual bajo demanda.
Hace la conversión automáticamente del tipo de atributo
Permite indicar valores por defecto en caso de que el atributo no exista
Daniel Ponsoda Montiel
97
8.3 Sistema de consola cliente / servidor
Figura 8.3. Diagrama de clases. Sistema de consola cliente / servidor
El cliente/servidor de consola permite controlar el reproductor del motor desde un PC
remoto. Las características son:
Permite ejecutar scripts sobre el dispositivo de pruebas desde la máquina de desarrollo.
Permite enviar/recibir archivos de assets.
Con una sola orden puede comprobar cambios de forma recursiva en el directorio de
trabajo y enviar al dispositivo de prueba todos los ficheros con fecha de modificación
actualizada.
Permite escribir y ejecutar en tiempo real, lo que posibilita ver cambios de código en
vivo mientras el juego está ejecutándose.
Game engine multiplataforma
98
8.4 Entidad-Componente-Sistema
Figura 8.4. Diagrama de clases. Entidad-Componente-Sistema
El patrón arquitectural Entidad-Componente-Sistema ha sido el elegido para dar forma a
nuestro motor. Para más información acerca de este patrón de diseño, las características y
ventajas que proporciona y en qué consiste cada uno de los componentes que lo conforman,
ver el capítulo de “Definición de la arquitectura”. Además, en el apartado “Implementación”
podemos ver algunos cambios que se han realizado sobre el patrón original y su justificación.
Daniel Ponsoda Montiel
99
8.5 Sistema de gráficos
Figura 8.5. Diagrama de clases. Sistema de gráficos
El sistema de gráficos es a quien se delega el control sobre la librería OSG, de ahí que en
muchas de las clases que vemos en este diagrama aparezcan tipos de esta librería. Este sistema
forma parte del ECS y se encarga de objetos de tipo dibujable, cámaras y luces, es decir, todo
lo que tiene que ver con el control de los gráficos.
Game engine multiplataforma
100
8.6 Sistema de entrada
Figura 8.6. Diagrama de clases. Sistema de entrada
Este sistema se encarga de recibir la entrada de usuario. En nuestro caso, teclado y ratón
en caso de la versión de escritorio o acelerómetro y pantalla táctil en la versión de móvil. Para
la pantalla táctil, el control se deriva al listener del ratón.
También dispone de un emulador de acelerómetro que se utiliza en la versión PC para
hacer pruebas utilizando el teclado. El eje X se controla con [I,K], el Y con [Y,H] y el Z con
[J,L].
Excepto el control de acelerómetro, los demás eventos utilizan internamente la cola de
eventos de OSG. De hecho, la clase InputSystem en realidad hereda de osg::GUIEventHandler.
Por último, nótese que este sistema es independiente del conjunto ECS. No obstante, es
utilizado por GraphicsSystem ya que es de quien se obtiene la instancia de la ventana OSG
que nos da los eventos.
Daniel Ponsoda Montiel
101
8.7 Sistema de log
Figura 8.7. Diagrama de clases. Sistema de log
El log se utiliza para enviar por consola o a un fichero mensajes de error, avisos, mensajes
de depuración, o cualquier información de interés. Para ello se emplea un sistema de mensajes.
Las principales características son:
Imita el interfaz de stream de C++ (como cout)
Identifica fichero y número de línea de donde se ha hecho el log
Puede derivar la llamada a múltiples salidas: Consola, fichero, ventana del IDE, etc.
Los logs de depuración se pueden eliminar en la compilación release
Game engine multiplataforma
102
8.8 Sistema de física
Figura 8.8. Diagrama de clases. Sistema de física
El sistema de física utiliza internamente la librería Bullet y proporciona un interfaz
simplificado que facilita al programador la creación de objetos con propiedades de física como
masa, gravedad, velocidad lineal o angular e impulso. Además, proporciona un interfaz
CollisionListener que se puede utilizar para capturar una colisión entre dos entidades cuando
ésta se produce. El método proporciona además los puntos de contacto de las entidades en
colisión.
La configuración del objeto es también muy sencilla con solo 4 flags combinables:
DYNAMIC: Crea un objeto con comportamiento de movimiento dinámico clásico.
GHOST: El objeto responde a colisiones (lanzando el evento) pero ningún objeto del
entorno de física puede moverlo. Si alguno lo toca, simplemente lo atravesará (de ahí
el nombre “fantasma”).
TRIGGER: Para poder obtener colisiones con un listener, debemos activar este flag.
KINEMATIC: Permite que el objeto se mueva manualmente acorde con la matriz de
transformación procedente de TransformComponent.
Daniel Ponsoda Montiel
103
8.9 Sistema de scripting
Figura 8.9. Diagrama de clases. Sistema de scripting
El sistema de scripting gestiona los scripts del programador de juegos y se apoya en lua a
través de la librería de binding Sol2 (de ahí los objetos sol* que aparecen en el diagrama).
Como vemos, la clase sistema tiene un objeto InputSystem que utiliza para poder notificar
a las distintas instancias del sctipt de los eventos de entrada de usuario. El ScriptComponent
es un contenedor de clases e instancias de objetos Lua. La instancia es realmente sobre la que
se trabaja en cada iteración llamando a los eventos correspondientes (update, init o cleanup).
También vemos que es posible obtener el valor “self” que sería como el objeto “this” en C++
para instancias de clase. Y por último, es posible acceder a miembros de instancia del objeto
Lua con setValue, getValue o getFunction.
Game engine multiplataforma
104
8.10 Sistema de transformación
Figura 8.10. Diagrama de clases. Sistema de transformación
El sistema de transformación se utiliza para todos los objetos que ocupen un espacio en la
escena. Es otro de los sistemas del ECS y básicamente es el nexo de comunicación entre el
sistema de física y el de gráficos. Su función es muy sencilla: Simplemente guarda una matriz
de transformación que podemos modificar o consultar utilizando los métodos que se
proporcionan para posición, escala y rotación.
Daniel Ponsoda Montiel
105
9 IMPLEMENTACIÓN
Una vez tenemos la arquitectura y el diseño preliminar definidos, y ya hemos compilado
todas las dependencias y preparado nuestro entorno de desarrollo, nos disponemos a escribir
el código del motor. Todo lo que habíamos planteado de manera abstracta a nivel teórico va
a ponerse a prueba en este momento y, en muchos casos, no encajará en la práctica como nos
habíamos imaginado.
En este apartado se detallarán las cuestiones más importantes en cuanto a decisiones y
cambios en el diseño que se han tenido que adoptar durante la fase de desarrollo, explicando
por qué se han realizado de determinada forma y no de otra, de modo que pueda resultar útil
a cualquier desarrollador que se plantee un reto similar.
9.1 Sistema de log Para empezar con buen pie debemos disponer desde el principio de herramientas que nos
hagan la vida más fácil durante el resto del desarrollo. Tener un buen sistema para hacer
trazas y depurar es vital. Por este motivo lo primero que se implementó fue un completo
sistema de log que además nos resolviera algunos problemas encontrados.
9.1.1 Necesidad de derivar la salida de log a múltiples destinos Al ser un proyecto multiplataforma que se desarrolla en distintos IDE (Android Studio,
Visual C++ y Qt Editor), un problema que surge es que la ventana de depuración no muestra
lo que se escribe por la salida estándar, con lo cual ya no podemos usar los clásicos printf o
cout para mostrar información de depuración, sino que tenemos que ceñirnos al API propio
del sistema en cuestión. Por ejemplo, en Visual Studio, para poder ver algo en la ventana de
Game engine multiplataforma
106
salida deberíamos usar OutputDebugString del SDK de Windows, mientras que en Android
emplearíamos la función __android_log_print del SDK nativo (NDK de Android).
Esto se ha resuelto utilizando una clase Log que implementa un sistema de notificaciones
al cual podemos suscribir los listeners que necesitemos. Por ejemplo, en la versión Windows
añadiríamos LogVS (para ver el log por la ventana inmediato de Visual Studio), LogConsole
(para mostrarlo también en el terminal, lo cual es útil para la consola de script), e incluso
LogFile, que deriva todo el log a un archivo de texto. En el capítulo de “Diseño del código”
podemos ver con más detalle el diseño final del sistema de log.
Por supuesto, se podría haber resuelto de otras formas más sencillas, como por ejemplo
mediante una función global que, con directivas del preprocesador (#iddef) seleccione el
método adecuado en función de la máquina objetivo para la cual se compila, pero, si lo
hiciéramos así, el código quedaría menos claro con todo en una misma función y, por otro
lado, el usuario no podría activar/desactivar dinámicamente el log a fichero. Además, como
veremos a continuación, este sistema de log resuelve otras cuestiones que, de hacerlo así, irían
complicando esta función global.
9.1.2 Log como stream con información de fichero y línea Al utilizar una función propia que hace el log debemos encontrar una solución para poder
convertir fácilmente una variable a texto y así poder imprimirla.
Para ello se imita el sistema de streams de C++ de manera que con el operador <<
podemos concatenar cualquier tipo básico o cualquier clase que lo tenga sobrecargado.
La solución no está exenta de cierto ingenio. La clase Logger implementa un constructor
al cual se le pasa el nivel de mensaje y el fichero fuente y el número de línea (usando las
macros estándar __FILE__ y __LINE__ respectivamente). Luego, el método Log recibe el
“tag” o etiqueta del mensaje (útil para filtrar el log en un archivo de texto) y devuelve un
objeto de tipo ostream al que ya podemos añadir datos con <<.
El funcionamiento interno para loguear un mensaje sería por ejemplo así:
Daniel Ponsoda Montiel
107
Logger(Message::MSGWARNING, __FILE__, __LINE__).Log("prueba") << "Variable de prueba: " << miVariable;
Lo que ocurre en esta línea es lo siguiente:
Se instancia un objeto Logger con los parámetros indicados (tipo, fichero y línea)
Se llama a Log con el tag “prueba” y éste devuelve un ostream
El ostream se usa para concatenar la información que se pasa
Al no haberse asociado a ninguna variable, la instancia Logger se destruye al terminar
de ejecutar la línea
En el destructor se usa el ostream (que está como miembro privado de la clase) donde
tenemos los datos y se notifican éstos a todos los lísteners contenidos en el notificador
(estático) de la clase Logger antes de destruir la instancia.
Finalmente, para poder manejar esto con facilidad, se crean unas macros de este estilo:
#define TLOGE(x) \ ion::log::Logger(ion::log::Message::MSGERROR,__FILE__,__LINE__).Log(x) #define TLOGW(x) \ ion::log::Logger(ion::log::Message::MSGWARNING,__FILE__,__LINE__).Log(x) #define TLOGI(x)\ ion::log::Logger(ion::log::Message::MSGINFO,__FILE__,__LINE__).Log(x)
Ahora, para hacer lo mismo que antes, simplemente escribiríamos:
TLOGW("prueba") << "Variable de prueba: " << miVariable;
El tag es opcional. Podemos también hacer un log sin etiqueta usando las mismas macros
pero sin el prefijo T:
LOGW << "Variable de prueba: " << miVariable;
9.1.3 Desactivación de logs de depuración en modo release Otro problema clásico de introducir líneas de depuración en nuestro código es que podemos
dejarnos olvidadas algunas en la versión release. Esto podría revelar datos no deseados al
usuario final, además de que, según donde aparezcan, podría decrementar el rendimiento.
Para solucionarlo, diferenciamos los logs de error, warning, o info (con LOG) de los que
son sólo para depuración (con LOGD). Esta función LOGD está definida en una macro que
Game engine multiplataforma
108
desaparece sin rastro deshabilitando el flag de compilación ION_LOG_LEVEL_DEBUG en
modo release. Los logs de debug no se añadirán a la compilación.
A priori, esto no están sencillo porque si tenemos en cuenta lo explicado en el apartado
anterior, si decidimos hacer algo como esto:
#ifdef ION_LOG_LEVEL_DEBUG #define LOGD(x) \ ion::log::Logger(ion::log::Message::MSGDEBUG,__FILE__, __LINE__).Log(x) #else #define TLOGD(x) #endif
Al desactivar ION_LOG_LEVEL_DEBUG tendríamos inmediatamente errores de
compilación en los logs, ya que por ejemplo este código:
LOGD << "Variable de prueba: " << miVariable;
Se convertiría en esto:
<< "Variable de prueba: " << miVariable;
La solución ha sido añadir un código sustituto que no moleste al compilar cuando no se
usa el debug como se muestra a continuación:
#ifdef ION_LOG_LEVEL_DEBUG #define LOGD(x) \ ion::log::Logger(ion::log::Message::MSGDEBUG,__FILE__, __LINE__).Log(x) #else define TLOGD(x) if (true) {} else std::cerr #endif
Ahora, el mismo código de antes quedaría así al hacer la sustitución:
if (true) {} else std::cerr << "Variable de prueba: " << miVariable;
Esto no solo compila sin problemas, sino que, si el compilador está realmente en modo
release, al aplicar la optimización no generará ningún código, puesto que detectará que la
condición del “if” se cumple siempre y por ello omitirá la parte del “else” y no la añadirá al
binario.
Daniel Ponsoda Montiel
109
9.2 Administración de recursos de disco Al instanciar una entidad en nuestro motor es muy habitual hacerlo a partir de uno o
varios assets que se cargan desde disco, como puede ser un shader, una malla 3D o un script.
Una forma “naive” de implementar esto para una posible factoría de entidad que recibe como
parámetro el nombre de archivo del objeto a dibujar podría quedar así:
entity = ion:addEntity("jugador", "heroe3D.obj")
Como resultado, la factoría instanciaría la entidad “jugador” y luego cargaría desde disco
el objeto “malla3D.obj” y se lo añadiría como un componente gráfico a la entidad.
Por supuesto, esto no es una forma correcta de administrar los recursos de disco, ya que
podría tratarse de un objeto muy recurrente como, por ejemplo, un enemigo que se instancia
100 veces en una escena. Con la misma factoría anterior, el siguiente código accedería a disco
100 veces para cargar “enemigo3D.obj”:
for i=1,100 do enemigos[i] = ion:addEntity("enemigo" .. i, "enemigo3D.obj") end
En realidad, esto es matizable, ya que el disco dispone de una caché que podría ayudar en
este caso, pero no siempre podemos confiar en que la cache de disco esté como nosotros
queremos. Por ejemplo, podemos cargar el enemigo al principio del programa y más tarde,
tras la carga de varios recursos o incluso debido a tareas internas del propio S.O., el archivo
inicial ha desaparecido de la caché.
Por este motivo, se hace imprescindible utilizar un sistema propio de caché que guarde
instancias de recursos cargados desde disco. Podríamos hacer esto en la propia factoría de la
entidad, pero entonces no podríamos reutilizar el sistema fuera de dicha factoría.
La primera solución sería separar esto en una clase separada AssetMapper que tenga un
contenedor con los assets previamente cargados e instancias de cargadores especializados
(ShaderLoader, MeshLoader…) a partir de un interfaz común AssetLoader. Luego, según el
tipo de archivo (mirando la extensión) usaríamos un loader u otro.
Game engine multiplataforma
110
El siguiente diagrama muestra cómo quedaría el diseño inicial simple de esta solución:
Para mantener el principio open-closed, estos loaders estarán mapeados por extensión de
archivo para no tener que comprobar a mano (mediante ifs) qué loader se debe usar en cada
caso.
Otro inconveniente que queremos resolver con esta clase es poder liberar fácilmente de la
caché entidades que ya no vamos a utilizar. Para lograrlo, teniendo en cuenta que para su
almacenamiento estamos usando Smart pointers, crearemos un método removeUnused en
AssetMapper que, por cada recurso, obtendrá el número de objetos referenciados y, si este
número es 1 significa que sólo está en la caché y por tanto se puede eliminar con seguridad.
Este método podrá ser llamado manualmente por el programador en el momento adecuado
como, por ejemplo, tras finalizar una zona o pantalla del juego.
Daniel Ponsoda Montiel
111
Un último detalle que queremos resolver es el hecho de que ahora el sistema devuelve los
assets como objetos de interfaz, por lo que al obtenerlos no pueden ser usados directamente
sin hacer el correspondiente up-casting. Por ejemplo, en la parte de C++, para obtener un
script y luego acceder al código deberíamos hacer esto:
AssetPtr asset = assetsMapper->get("logica.lua"); ScriptClassPtr miScript = dynamic_pointer_cast<ScriptClass>(asset); string codigo = miScript->getCode();
Como vemos es algo incómodo, y además al ser un casteo dinámico queda en manos del
programador la responsabilidad de elegir el tipo de puntero que necesita. Además, este
inconveniente se traslada también a la hora de hacer el binding en Lua, ya que este lenguaje
no entiende de herencia de objetos o polimorfismo.
Por este motivo, lo que nos interesará será que sea el método get de assetMapper quien
nos proporcione el objeto listo para usar. Para ello, en primer lugar, haremos que el método
sea una plantilla que reciba el tipo que queremos. Ahora, el código anterior se puede simplificar
así:
ScriptClassPtr asset = AssetMapper->get<ScriptClass>("logica.lua"); string codigo = miScript->getCode();
Pero esto sólo deriva el mismo problema a la clase AssetMapper. Sin embargo, ahora
podemos resolverlo creando un nuevo nivel de indirección apoyándonos en el patrón composite.
La clase AssetMapper ya no contendrá los assets y loaders, sino que habrá una clase
AssetContainer dedicada a esto. De esta manera AssetMapper será un almacén que indexa los
AssetContainers por tipo de asset a cargar (es decir el typeid de la clase). Así, al recibir el
parámetro de tipo por plantilla, sólo tenemos que obtener el AssetContainer correspondiente
que tendrá los cargadores adecuados.
Esto también nos da mejor rendimiento al segmentar los assets por tipo y, además permite
tener múltiples tipos de cargadores para un mismo tipo de asset (por ejemplo, para texturas,
podemos tener cargadores png, jpg, gif, etc.). En el capítulo de “Diseño del código” podemos
ver el diagrama UML final para esta implementación.
Game engine multiplataforma
112
9.3 Dependencias en el sistema ECS El patrón ECS ofrece un diseño flexible, reusable, modular y eficaz, por lo que es ideal
para nuestro motor. Sin embargo, no todo queda resuelto, y es que esta arquitectura no evita
la necesidad de comunicación entre los distintos sistemas. El ejemplo más representativo tal
vez sea el sistema de script que, según el nivel de control que queramos dar, deberá estar
acoplado a prácticamente todos los componentes para saber la posición de un personaje o el
estado de una colisión, etc.
Para evitar este acoplamiento, la especificación del patrón nos dice que se debería resolver
mediante un sistema de mensajes para comunicar los sistemas empleando el patrón observer,
evitando así también un polling constante cuando no hay cambios de información. En cualquier
caso, como veremos en la implementación, habrá casos en los que, por optimización, no
tendremos más remedio que acoplar algunos componentes entre sí.
Un motor de juegos debe rendir al máximo en tiempo real y no puede permitirse la carga
que le añadiría un sistema de mensajes para componentes que van a estar necesariamente
comunicados como son los procedentes de los sistemas de transformación, gráficos y física.
Por este motivo, se ha hecho una modificación sobre el patrón original permitiendo el
acoplamiento de manera excepcional para sistemas como estos donde la comunicación es
constante. De esta forma, tanto el componente de física como el de gráficos tendrán ambos
una instancia del componente de transformación de la entidad a la que pertenecen.
Además, de cara a la creación del interfaz de las entidades para el script también será
cómodo para el desarrollador de juegos tener en el script de control de la entidad un objeto
entidad (lógico) que contenga punteros a los distintos componentes de ésta. De esta forma, el
script actuaría de mediador entre los componentes, añadiendo la lógica necesaria en cada
momento. Así pues, el diagrama de dependencias quedará como se muestra a continuación:
Daniel Ponsoda Montiel
113
Como vemos, el componente de transformación sirve de intermediario entre los de gráficos,
colisiones y física, permitiendo que se mantengan desacoplados entre sí.
9.4 Interoperabilidad entre librerías de gráficos y física Como ya hemos visto, este motor integra la librería OSG para gráficos y Bullet para física.
Por un lado, esto nos adelanta el trabajo, ya que no tenemos que implementar un motor
gráfico o de física desde cero, lo cual podría derivar de por sí en dos grandes proyectos
separados de éste.
Como desventaja, al ser dos librerías creadas por distintos autores, presentan problemas
de interoperabilidad entre sí. Esto añadido al hecho de que son dos partes del motor que están
altamente relacionadas, ha requerido de una solución eficiente para resolver el problema de
compatibilidad que introducen.
Para empezar, como vemos en la siguiente ilustración, utilizan sistemas de coordenadas
distintos:
Figura 9.1. Diagrama de dependencias entre componentes
+Z
Bullet OSG
+Z
+Y
+X +X
+Y
Figura 9.2. Sistemas de coordenadas en Bullet y OSG
Game engine multiplataforma
114
Esto nos obliga a tener en el componente Transform algún método para convertir las
matrices de un sistema a otro para que los cálculos sean equivalentes al hacer de puente entre
los sistemas de gráficos y física.
Lo haremos en el método onUpdate, que es cuando se actualiza la transformación (una vez
por fotograma). No se hace en los métodos que modifican la matriz como setPosition,
setRotation o setScale, ya que la lógica del juego podría hacer llamadas a estas funciones
varias veces teniendo que calcular todo el tiempo la matriz cuando sabemos que los gráficos
sólo se van a actualizar al final de la iteración.
Otra decisión importante que se ha tomado ha sido almacenar la transformación en el
componente como una instancia de matriz del sistema de física (Bullet) y convertirla al sistema
de gráficos (OSG) al hacer update. Esto se hace por motivos obvios, y es que los gráficos no
escriben en la matriz, sino que sólo la leen para hacer los cálculos. Sólo la lógica y la física
modifican este objeto.
Por otro lado, ambos sistemas emplean su propia librería de matemáticas, es decir, su
propio conjunto de clases para el manejo de vectores, matrices y cuaterniones. Para poder
tener un objeto que exponer al sistema de script de forma que sea coherente se han creado
clases que implementan el patrón Adapter para los objetos necesarios. Además, se han
sobrecargado los operadores de casteo de tipos para que sea sencillo comunicar una librería
con otra usando el mismo objeto.
9.5 Resolución de riesgos de programación concurrente Los riesgos de concurrencia aparecen cuando dos o más procesos simultáneos tratan de
escribir en la misma zona de memoria, o bien cuando uno lee mientras otro puede escribir. El
valor leído o escrito queda indeterminado y dependerá de quién llegó antes, lo cual no podemos
saberlo a priori.
El proyecto tiene dos puntos de riesgo críticos en cuanto a concurrencia de procesos que
pasamos a resolver en los siguientes apartados: uno es el input de usuario en la versión Android
que se hace en un hilo aparte, y otro es la ejecución de scripts desde la consola remota.
Daniel Ponsoda Montiel
115
9.5.1 Sistema de input de usuario Un input de usuario es, por ejemplo, la pulsación de una tecla o un click sobre la ventana
del juego o bien, en Android, un toque en la pantalla o un movimiento del sensor acelerómetro.
La versión de escritorio no tiene ningún problema en este sentido, puesto que de este
apartado se encarga la librería OSG, la cual utiliza un sistema de notificaciones que acumula
todas las peticiones en una cola para luego atenderlas una vez por fotograma de manera
síncrona antes de la ejecución de la iteración en el bucle principal del motor.
Sin embargo, en Android esto es distinto ya que, el método que nos da el SDK en este
sistema operativo trabaja de forma asíncrona en un hilo diferente del principal.
Inicialmente atendíamos el evento en el mismo momento en que nos llegaba desde el API
de Android. De esta forma, al recibirlo, se derivaba la llamada a todos los listeners registrados
a dicho evento y que esencialmente pertenecen a objetos instanciados en el sistema de script.
El problema de esto es que uno de los scripts destino podía estar en ejecución en ese momento
y, al hacer esa llamada, se ejecutaba el método correspondiente simultáneamente dando
resultados impredecibles en los datos.
En este caso se ha resuelto creando un método en la clase InputSystem que atiende las
peticiones procedentes de Android y sólo guarda los valores obtenidos para luego llamar a
todos los lísteners en orden al comienzo del bucle principal tal como lo hace OSG.
9.5.2 Ejecución de script desde consola remota El servidor de consola funciona mediante TCP y atiende las peticiones remotas del cliente
de forma paralela al núcleo del motor, ya que de otro modo se bloquearía al aceptar conexiones.
Si ejecutamos la orden Lua en el momento en que se recibe ocasionaríamos un problema
similar al descrito en el apartado anterior. Como solución vamos a adoptar un sistema
parecido, haciendo que la ejecución del script se posponga a un momento seguro, es decir, al
comienzo de la siguiente iteración del motor.
Game engine multiplataforma
116
Así pues, cuando se hace la petición, se guarda la orden para el próximo frame y se ejecuta
en ese momento dejando en espera el servidor hasta que se produce una respuesta del sistema
de script. Dicha respuesta, que puede ser una cadena de texto con el log o un mensaje de error
en caso de que no compile el script, se enviará de vuelta al cliente.
Aunque así podría quedar resuelto, en nuestro caso daremos una nueva vuelta de tuerca y
esta vez sí que vamos a hacer uso de regiones de código mutex (mutuamente excluyentes)
para evitar problemas en el caso de que dos consolas (de dos programadores diferentes) hagan
una petición al mismo tiempo. Lo que bloquearemos en este caso será la variable que guarda
la cadena de script, así como la cadena de respuesta para que no puedan interferirse dos
consolas.
9.6 Sincronización de relojes Una función importante del cliente/servidor de consola es que es capaz, bajo demanda, de
detectar los cambios en la fecha de modificación en los ficheros del directorio de trabajo para
subir automáticamente al reproductor los cambios recientes.
Esta utilidad es muy potente ya que, de este modo podemos editar los ficheros con nuestro
programa favorito, por ejemplo, los scripts con un editor Lua o las mallas 3D de los objetos
con Blender y, tras guardar los cambios, sólo con una orden se transmitirán los archivos
actualizados inmediatamente al dispositivo de prueba.
Esto no es tan trivial como parece en un principio, ya que para que el sistema pueda saber
qué ficheros están actualizados en el cliente con respecto a la versión que ya está en el destino,
los relojes de ambas máquinas deben estar sincronizados, pues la fecha que se guardará al
recibir el fichero por red será la fecha en la que se recibe (y no la fecha del archivo original)
ya que se crea en ese momento.
La solución más eficaz es sincronizar ambos relojes empleando algún protocolo conocido
como NTP, aunque en nuestro caso utilizaremos un sistema a medida más sencillo que sirve
sobradamente para nuestro caso.
Daniel Ponsoda Montiel
117
La solución consiste en guardar en el dispositivo de prueba un archivo sync.txt donde se
irán guardando las fechas de modificación originales de los ficheros transmitidos. A la hora de
comprobar si un fichero está actualizado será esa hora la que compararemos.
Como vemos esta hora se guarda en disco y no simplemente en un array de la memoria,
dado que, de hacerlo así, el sistema no nos serviría entre distintas sesiones de trabajo ya que
los datos desaparecerían.
9.7 Abstracción mediante fachadas Como ya hemos visto, el patrón ECS es muy flexible y modular, lo cual nos da un código
escalable, extensible y fácil de mantener. Como contraprestación, estas ventajas vienen a
cambio de una mayor complejidad en el uso del código.
El siguiente extracto de script Lua sería el código necesario para crear una entidad
utilizando puramente el interfaz que nos proporciona nuestro sistema ECS:
-- Instanciar una nueva entidad entity = Entity.new('caja') -- Crear un volumen que representa el objeto a dibujar (en este caso una caja) box = Box.new(2,2,2) -- Definir y añadir componente de transformación con posición y rotación transform = TransformComponent.new() transform.position = Vector3.new(0,4,0) transform:setAxisAngle (Vector3.new(0.1,2.5,2.5), 2) entity:addComponent (transform) -- Crear un material indicando shader y color difuso shader = AssetMapper:getShader ('basic.glsl') material = Material.new() material.shader = shader material.setVector4('diffuseColor', Vector4.new(0,1,1,1) ) -- Definir y añadir componente de gráficos drawable = DrawableComponent.new() drawable.shape = box drawable.material = material entity:addComponent(drawable) -- Definir y añadir componente de física physics = PhysicsComponent.new() physics.shape = box physics.mass = 1.0 physics.flags = DYNAMIC | TRIGGER entity:addComponent(physics) -- Añadir entidad a la escena ion:addEntity(entity)
Game engine multiplataforma
118
Todo este código lo que hace es crear una caja de 2x2x2 con propiedades de física que
veremos cayendo en pantalla por efecto de la gravedad. Como resumen, lo único que hace es
instanciar una entidad en blanco a la que luego añade los componentes pertinentes con las
opciones elegidas. Finalmente, añade la entidad a la escena.
Este código que parece engorroso, en realidad ilustra sólo una parte de la cantidad de
parámetros y ajustes que están a nuestro alcance, pero no todos. Por ejemplo, aquí no se ha
añadido ningún componente de script para añadirle un comportamiento de lógica a la entidad.
Sin embargo, no siempre vamos a necesitar tanto nivel de detalle. Por este motivo, se ha
decidido añadir una capa de abstracción para la creación y manejo de entidades que nos sirva
como fachada para facilitar el trabajo. Así pues, por ejemplo, podemos escribir un código
equivalente al anterior utilizando la función ion:addPhisicsEntity:
entity = ion:addPhysicsEntity("caja", BoxVolume(2,2,2), DYNAMIC | TRIGGER, PositionRotation(V4(0,4,0), V4(0.1,2.5,2.5))) entity.drawable.material.diffuse = v4(0,1,1,1)
Las factorias BoxVolume y PositionRotation ayudan a identificar el significado de los
parámetros.
El resto de opciones que no aparecen, como la masa a 1.0 estarían establecidas por defecto.
Así, se crean todos los componentes necesarios con los parámetros indicados y se añaden a la
entidad, la cual aparece en la escena tras la llamada a esta función. El único código que queda
fuera de ella el que establece color, que se añade en una línea posterior.
Si además quisiéramos que la entidad tuviera, por ejemplo, los scripts “ControlJugador”
y “Planear”, lo haríamos así:
entity = ion:physicsEntity("caja", BoxVolume(2,2,2), DYNAMIC | TRIGGER, PositionRotation(V4(0,4,0), V4(0.1,2.5,2.5))) entity.drawable.material.diffuse = v4(0,1,1,1) ion:addScript(entity,"ControlJugador") ion:addScript(entity,"Planear") ion:addEntity(entity)
En el futuro, la función addScript se reemplazará por entity:addScript. No se ha hecho
todavía porque requeriría comprobaciones, pues en este momento no se puede añadir un script
Daniel Ponsoda Montiel
119
a una entidad que ya ha sido añadida a la escena. Por eso se debe añadir antes de la llamada
a addEntity (obsérvese que al instanciar la entidad hemos usado physicsEntity en lugar de
addPhysicsEntity como antes).
9.8 Acerca del editor gráfico Por motivos de tiempo externos al desarrollo del proyecto, no ha sido posible terminar
esta parte, siendo, de todo lo que inicialmente se propuso, la única tarea que se pospone para
terminar en el futuro. No obstante, sí que se ha dejado un buen tramo del camino recorrido,
pues la aplicación base del editor está diseñada y desarrollada, dejando también el entorno de
desarrollo preparado. Se trata de una aplicación Qt de escritorio que muestra una ventana
OpenGL, la cual se ha conectado al núcleo del motor.
A partir de aquí lo siguiente sería añadir los botones, menús y otros controles imitando el
mockup propuesto en esta memoria. Todo ello está listo para realizar con el editor gráfico
QtEditor.
Daniel Ponsoda Montiel
121
10 EJEMPLOS DE USO
Esta sección pretende servir como una introducción rápida para el manejo del motor por
parte del programador de juegos que quiera empezar a realizar un nuevo proyecto con nuestro
sistema. En los siguientes apartados explicaremos cómo crear un proyecto básico y también
analizaremos el código de las demos que acompañan al motor con el fin de que todo quede
más claro.
Para ampliar la información que se proporciona aquí y tener una referencia completa de
toda la funcionalidad disponible, nos remitiremos al “Manual del API” que se incluye en el
anexo III.
10.1 Primer proyecto Para empezar un proyecto simplemente copiamos el ejecutable del reproductor
“player.exe” a la carpeta que queramos. Ese será nuestro directorio de trabajo. También
copiaremos ahí el directorio “shaders” de ejemplo que luego podremos modificar.
Ahora, creamos ahí mismo una carpeta “scripts” donde añadiremos un archivo con el
nombre “main.lua”. Debe llamarse así para que actúe como punto de entrada y se ejecute
automáticamente al abrir el juego.
El proyecto está listo para ejecutarse, pero aún no hace nada porque no hemos añadido
ninguna entidad a la escena. Si lo arrancamos ahora veremos una pantalla vacía.
Nuestra escena de ejemplo contendrá un suelo, una caja y una cámara. Veamos cómo
crearlas.
Game engine multiplataforma
122
Para crear el suelo añadimos el siguiente código a “main.lua”:
ground = ion:addPhysicsEntity("ground", BoxVolume(20,1,20), KINEMATIC) ground.physics.mass = 0.0
Como vemos, lo creamos como KINEMATIC y luego establecemos la masa a 0. Esto es
para que podamos colocarlo en una posición fija sin que reaccione a las fuerzas físicas con
otros objetos.
Ahora crearemos una caja que caerá al suelo por efecto de la gravedad. El código es muy
similar:
box = ion:physicsEntity("box", BoxVolume(2,2,2), DYNAMIC, PositionRotation(V3(4,8,0),V3(1,0.5,1.2))
Este objeto se ha creado con las opciones físicas por defecto (DYNAMIC con mass=1)
Por último, haremos que la cámara mire a la posición donde caerá la caja:
cam = ion:getEntity("camera").camera
cam:lookAt(V3(0,25,50),V3(0,0,0),V3(0,1,0))
Con esto ya tenemos nuestro primer proyecto listo para ejecutarse. Si todo ha ido bien,
veremos una caja caer por efecto de la gravedad tal como se muestra en la siguiente captura:
Figura 10.1. Captura del primer proyecto de ejemplo
10.2 Uso de la consola Antes de pasar a explicar las demos, vamos a introducir el uso de la consola para poder
ejecutar scripts.
Daniel Ponsoda Montiel
123
En esta ocasión, vamos a ejecutar player.exe desde el mismo directorio de trabajo original
del motor para así tener acceso a todos los assets que vamos a necesitar. Una vez abierto,
veremos la ventana vacía ya que no tenemos un main asociado, pero esto no importa porque
lo que queremos es ejecutar una orden de consola.
Para ello, abrimos una ventana de terminal y ejecutamos console.exe utilizando como
servidor nuestro host local (127.0.0.1) con el siguiente comando:
console 127.0.0.1
Esto conectará con el player (siempre que esté abierto) para poder enviar órdenes con
mediante Lua. Si todo ha ido bien debería mostrar los parámetros de inicio seguidos de un
mensaje de bienvenida:
IP: 127.0.0.1 Port: 8080 Root: d:/tfg/player/player/bin Welcome to ION Engine Console. Copyright (C) 2019. Daniel Ponsoda Montiel. Ready >
Ahora podemos escribir la orden que queramos. Por ejemplo, podemos probar el uso del
entorno lua haciendo algun a operación matemática utilizando variables. Veamos un ejemplo:
Ready > a = 1 Ready > b = 2 Ready > print(a + b) 3
En los siguientes apartados veremos cómo usar la consola para ejecutar las demos, pero el
verdadero potencial de esta herramienta se demuestra sabiendo que tenemos a nuestro alcance
todo el API del motor desde la consola, con lo que podemos hacer cambios en vivo en el código
mientras está en ejecución para hacer todas las pruebas que sean necesarias para pulir nuestro
proyecto de forma rápida y eficaz.
Game engine multiplataforma
124
10.3 Demostración acelerómetro: “DemoBall.lua” Esta demo incluida en el directorio scripts del motor nos enseña como obtener el estado
del acelerómetro de Android al tiempo que también nos demuestra cómo añadir un
componente de script a una entidad para alterar su comportamiento.
Para ejecutarla, accedemos a la consola como hemos visto en el apartado anterior y
escribimos la siguiente orden:
> ion:run("DemoBall")
Esto ejecutará el programa “DemoBall.lua” que se encuentra en el directorio “scripts” así
como los scripts asociados que necesita. En este momento deberíamos ver la siguiente imagen:
Figura 10.2. Demostración acelerómetro
Veremos una bola blanca sobre un escenario donde tiene un suelo y cuatro paredes para
que no se salga. Si el reproductor estamos ejecutándolo sobre un dispositivo Android,
podremos controlar la gravedad con el acelerómetro y ver cómo se mueve la bola.
Si lo hacemos sobre Windows, podemos probar el mismo comportamiento con el emulador
de acelerómetro (Las teclas J,K,L,I funcionan como los cursores para los ejes X,Z, y las teclas
Y,H hacen lo propio para el eje Y)
Veamos ahora con un poco más de detalle el funcionamiento de esta demo. En primer
lugar, vemos algo nuevo al principio del código:
AssetMapper.forceReload = true
Daniel Ponsoda Montiel
125
ion:reset()
El flag forceReload le dice al gestor de assets que acceda siempre a disco para obtener los
recursos sin utilizar caché. Esto es útil en modo desarrollo si vamos a estar haciendo cambios
en los assets. Así podemos ver los cambios sin tener que reiniciar el player.
Luego, ion:reset() lo que hace es reiniciar la escena (elimina todo lo que haya antes de
cargar este script). Como veremos en la siguiente demo, la ejecución de un script no limpia la
escena previamente. Esto nos sirve para poder ejecutar y combinar scripts entre distintos
archivos. Por otro lado, nos puede interesar reiniciar, por ejemplo, para un cambio de fase o
escena en nuestro juego. Para nuestro ejemplo, reiniciamos al comienzo.
Seguido a estas dos instrucciones, se añaden a la escena el suelo y las paredes tal como ya
se ha explicado y, por último, vemos cómo se crea la entidad “ball”. Esta vez, añadiendo un
componente de script:
ball = ion:physicsEntity("ball", SphereVolume(1), 0, Position(0,5,0)) ball.drawable.material.diffuse = V4(1,1.4,1,1) ion:addScript(ball,"AccelController") ion:addEntity(ball)
El script “AccelController.lua” contiene el siguiente código:
function AccelController:init() print('init AccelController\n') end function AccelController:update(deltaTime) end function AccelController:onAccelUpdate(x,y,z) g = 9.8*4 v = V3(x,-z,-y) v = v:normalize() v = V3(v.x*g, v.y*g, v.z*g) self.physics.gravity = v; end
Como vemos, simplemente tiene 3 funciones de objeto que responden a los eventos init,
update y onAccelUpdate. Los dos primeros no hacen nada, mientras que el último, que es el
que nos interesa, recibe los ángulos del acelerómetro desde Android y calcula la nueva
inclinación para modificar la gravedad sobre el propio objeto (self).
Game engine multiplataforma
126
Como vemos, los scripts que podemos usar como componentes de una entidad deben
implementar funciones dentro del objeto del mismo nombre que el archivo. Los distintos
eventos que se pueden utilizar están descritos en el “Manual del API” del anexo III.
10.4 Demostración rendimiento: “DemoRandom.lua” Esta demo instancia una cantidad previamente elegida de objetos con tamaños, formas y
colores aleatorios con propiedades de física que colisionarán entre sí en la escena. Esto nos
sirve para demostrar el rendimiento de nuestro motor.
Como escena base utilizaremos la demo anterior que nos proporciona un recipiente para
los objetos. Por este motivo, en el script esta vez no ponemos ion:reset() al comienzo. De esta
manera, tras ejecutar DemoBall, si lanzamos DemoRandom no se borrará la escena anterior
y los objetos caerán sobre ese suelo.
Una última cosa que haremos antes de ejecutar DemoRandom será indicarle el número de
entidades que vamos a crear por medio de la variable global maxEntities. Las órdenes a
ejecutar serán, por tanto, las siguientes:
> ion:run("DemoBall") > maxEntities = 200 > ion:run("DemoRandom")
Al hacerlo nos aparecerán 200 entidades aleatorias en escena que se mueven de manera
fluida en cualquier dispositivo de gama baja-media moderno:
Figura 10.3. Demostración física
Daniel Ponsoda Montiel
127
10.5 Demostración minijuego: “DemoMarble.lua” Esta demo reúne una gran parte de la funcionalidad del motor. En ella se implementa un
minijuego basado en el mítico Marble Madness de la época de los 8 bits en el que controlaremos
una bola por un escenario con obstáculos y enemigos utilizando el acelerómetro del móvil.
En esta ocasión, las características nuevas del API que pondremos a prueba serán:
Carga de modelos 3D creados en Blender (exportados a .obj)
Combinación de varios componentes de script sobre una misma entidad
Movimiento cinemático de elementos de la escena
Respuesta a eventos de colisión por parte de la lógica
Colisiones en objetos cóncavos
Como venimos haciendo hasta ahora, la demo la ejecutaremos por medio de la consola,
pero debemos recordar que, si este fuera un juego para distribuir, renombraríamos el script
“DemoMarble.lua” a “main.lua” para que se ejecutase automáticamente al abrir la aplicación.
La orden para lanzar la demo por ahora sería:
> ion:run("DemoMarble")
Tras una breve carga (no debería demorar más de 1 o 2 segundos), veremos en pantalla el
juego. Nuestro personaje que es la bola puede ser controlado con el acelerómetro. Deberemos
evitar caer desde demasiado alto o la caída nos matará. También debemos evitar que nos
toquen los enemigos, los cuales son unas bolas negras con pinchos que deambulan por la
escena. El objetivo es llegar a la zona cuadriculada que sería la meta (cuidado con el último
tramo, ya que es un puente muy estrecho). Al llegar al final no hará nada, puesto que sólo es
una demostración y no tiene más fases, pero seguro que puede servir de base e inspiración
para un juego más grande.
A continuación, mostramos algunas capturas. Como puede verse, el escenario tiene los
bordes redondeados. Esto es porque no ha sido creado usando las formas básicas como hasta
Game engine multiplataforma
128
ahora, sino que todo son modelos 3D diseñados en Blender. En la carpeta “meshes” están
todos los modelos que intervienen en la escena.
Figura 10.4. Juego de demostración
Daniel Ponsoda Montiel
129
11 PRUEBAS DE RENDIMIENTO
Las siguientes pruebas han sido realizadas utilizando la demo de rendimiento
“DemoRandom.lua” sobre la escena del tutorial de iniciación (Ver apartado de “Ejemplos de
uso”). La versión compilada del motor está optimizada para modo release con Visual Studio
Community 2017.
El equipo de escritorio utilizado para las pruebas tiene las siguientes características:
Procesador Intel Core i7 2600K Arquitectura 64 bits
Número de núcleos 4 Número de subprocesos 8
Velocidad de reloj 3800 Mhz Caché primaria 4x32Kb
Caché secundaria 4x256Kb Caché L3 8Mb SmartCache
RAM 2x4Gb DDR3 a 1333Mhz Velocidad bus DMI 5 GT/s
Ancho banda memoria 21 GB/s Disco duro 240Gb SSD Sata 3
Tarjeta gráfica nVidia Geforce GTX 750 Sistema operativo Windows 10 64 bits
Figura 11.1 DemoRandom.lua
Game engine multiplataforma
130
El dispositivo Android de pruebas es el siguiente:
Modelo Samsung Galaxy S4 GT-I9505 Procesador Krait 300
Número de núcleos 4 Velocidad de reloj 1900 Mhz
RAM 2Gb a 600Mhz GPU Qualcomm Adreno 320, 400Mhz
Sistema operativo Android 5.0.1
Para la medición se ha desactivado la sincronización vertical de la pantalla y se ha obtenido
el resultado más bajo registrado tras la ejecución de la demo. Dicho resultado coincide con el
momento donde aparecen todos los objetos en pantalla ya que, más tarde, a medida que van
cayendo éstos, se salen por debajo y ya no se renderizan con lo cual el rendimiento es mayor.
Los siguientes gráficos comparan la versión de escritorio con la versión móvil. La versión
PC aguanta sin bajar de 90 fps con 550 entidades simultáneas.
Figura 11.2. Gráfica de rendimiento en la versión PC
Por otro lado, como vemos, en la gráfica de rendimiento de Android, hemos tenido que
utilizar otra escala de entidades para poder apreciar mejor los datos. Si en PC hemos probado
entro 0 y 1000 entidades, en Android medimos de 0 a 100.
3566
1881
1449
1098824
604445
285 157 142 121 90 57 48 43 37 33 29 23 22 190
500
1000
1500
2000
2500
3000
3500
4000
FOTO
GRA
MAS
PO
R SE
GU
ND
O
NÚMERO DE ENTIDADES
Rendimiento versión PC
Daniel Ponsoda Montiel
131
Este sistema genera menos fps ya que en general la GPU y la CPU tienen mucha menos
potencia. No obstante, un juego normal con 30 entidades de física simultáneas es más de lo
necesario para casi cualquier propósito, y esa cifra se alcanza a 44 fps, lo cual compite sin
problemas con otros juegos comerciales de esta plataforma.
Figura 11.3. Gráfica de rendimiento en la versión Android
773
163
69 44 33 27 23 20 17 15 13
0
100
200
300
400
500
600
700
800
900
0 10 20 30 40 50 60 70 80 90 100
FOTO
GRA
MAS
PO
R SE
GU
ND
O
NÚMERO DE ENTIDADES
Rendimiento versión Android
Daniel Ponsoda Montiel
133
12 POSIBLES MEJORAS
El game engine desarrollado en este trabajo ha quedado funcionalmente muy completo,
aunque por más características implementadas que pueda llegar a tener, siempre podrán
añadirse más. En los siguientes apartados se proponen algunas características extra que sería
interesante incluir a corto/medio plazo.
12.1 Mejoras en el sistema de gráficos Lo primero que tal vez mejoraríamos al ver las imágenes del motor en comparación a otros
disponibles en el mercado es el aspecto gráfico. Sin embargo, esto se puede personalizar por
parte del programador de juegos añadiendo shaders o creando los suyos propios. Un
desarrollador gráfico experimentado dominará el lenguaje GLSL y podrá crear todo tipo de
efectos para sus juegos. De hecho, lo más probable es que ya tenga su propia biblioteca de
shaders que podrá emplear en este sistema. Pero algo que no estaría de más, no obstante, es
incluir una pequeña biblioteca más extensa que cubra al menos los efectos más fundamentales,
como materiales tipo metal, transparencia, plástico, dibujo animado, etc.
Dichos shaders también servirían a programadores menos experimentados que podrían
usarlos como punto de partida para modificarlos y crear los suyos propios.
12.2 Compilación del script El hecho de que la lógica del juego pueda ser desarrollada mediante un lenguaje de script
tiene todas las ventajas que ya se han comentado en el apartado de introducción de esta
memoria. Principalmente nos da mucha más agilidad a la hora de programar al no tener que
Game engine multiplataforma
134
compilar el código y, además, al usar un intérprete sobre una máquina virtual con garbage
collector, no tenemos que preocuparnos de problemas de pérdida de punteros, memoria, etc.
Sin embargo, tiene una desventaja y es que el rendimiento no puede compararse al de un
programa realizado en código nativo (por ejemplo, el que genera un compilador de C++).
Una mejora que se podría hacer sin demasiada dificultad a medio plazo sería la inclusión
de un sistema que permitiera compilar los scripts Lua a código máquina para la plataforma
objetivo. De esta forma, durante la etapa de desarrollo de un juego podríamos desarrollar con
Lua ya que nos da mayor soltura a la hora de programar y, cuando por fin hemos terminado
el proyecto y lo tenemos todo como queremos, hacer una compilación en binario para, además,
que funcione al máximo rendimiento posible.
Pero ¿Cómo conseguiríamos esto? Podríamos usar LuaJIT, pero este sistema no está
disponible en todas las plataformas y además tampoco ofrece el 100% del rendimiento de un
código escrito en C++. La idea sería crear, utilizando Bison y Flex, un traductor de Lua a
C++. Con los estándares modernos de C++ tenemos todo tipo de características en este
lenguaje como funciones lambda, smart pointers, etc. con lo que una traducción de Lua a
C++ se podría hacer prácticamente con un símple cambio en la gramática (sin apenas tocar
nada a nivel semántico), con lo que el proceso se simplifica enormemente.
Una vez tenemos el código traducido a C++ se compilaría utilizando un compilador
maduro como gcc o clang, el cual nos daría acceso a todo el potencial que ofrece su optimizador
de código. Además, al ser de código abierto, podría incluirse directamente en el motor.
12.3 Prescindir de algunas dependencias La unión en un solo sistema de OSG (para gráficos) y Bullet (para física) ha quedado
funcional, pero no es todo lo óptimo que desearíamos, ya que como ya hemos visto en
apartados anteriores, cada una de estas librerías utiliza su propio sistema de matemáticas y,
para poder comunicarlas entre sí, ha hecho falta recurrir a código adaptador, introduciendo
un nivel de indirección que produce pérdida de rendimiento.
Daniel Ponsoda Montiel
135
Por otra parte, OSG es un motor gráfico que no hace uso de la última tecnología disponible
como Vulkan o DirectX12.
Lo ideal sería desarrollar una librería de matemáticas propia partiendo de los
conocimientos que ya tenemos (ver Anexo I: Librería de matemáticas) y basarnos en dicha
librería para crear un motor gráfico que utilice Vulkan para la versión de escritorio, OpenGL
para Android (ya que la mayoría de dispositivos actuales todavía no soportan Vulkan) y
DirectX 12 para un posible port a Xbox.
Por último, teniendo también un motor de física propio tendríamos todo bajo el mismo
sistema de matemáticas y podríamos optimizarlo adecuadamente.
12.4 Cifrado del código y assets En juegos comerciales esto es imprescindible, ya que la gran mayoría de desarrolladores lo
demandan. Los assets y el código deben estar cifrados para dificultar la ingeniería inversa.
Sobre todo, para tener una mínima protección frente a hackers que puedan hacer trampas en
juegos de red y también, por supuesto, para proteger la propiedad intelectual de los creadores.
Daniel Ponsoda Montiel
137
13 CONCLUSIONES
Tras completar con éxito el desarrollo del motor, se ha conseguido realizar de manera
satisfactoria un tipo de proyecto que alberga un alto grado de complejidad y se ha demostrado
que un game engine básico puede ser desarrollado por una sola persona en un plazo razonable.
Por supuesto, cualquier programador cuyo objetivo sea realizar un videojuego podrá elegir
entre una gran cantidad de herramientas disponibles en el mercado, muchas de ellas excelentes,
de código abierto y con un inmejorable soporte técnico. No obstante, para hacer uso de ellas,
también es necesario saber lo que estamos haciendo y, sobre todo, sortear la curva de
aprendizaje que impone el motor que vayamos a usar. Pero al final, esto no será más que otro
tipo de esfuerzo que estamos invirtiendo en aprender a usar esa herramienta concreta.
Por otro lado, si desarrollamos nuestro propio motor, llegamos por fin a comprender qué
hay debajo de todos esos subsistemas que, para el resto de programadores, seguirán siendo
cajas negras cuyo funcionamiento es un misterio. Esto sin duda nos hace mejores ingenieros,
puesto que seremos capaces de extrapolar lo aprendido para plantear mejores soluciones en
caso de trabajar con cualquiera de los motores existentes, ya que los principios en los que se
basan son los mismos en su gran mayoría. Además, estaremos mejor predispuestos a realizar
mejoras sobre un motor existente de código abierto.
Por último, quisiera destacar la experiencia adquirida durante el desarrollo en cuanto a
diseño de código. Se trata de un sistema complejo donde hay que encajar piezas procedentes
de disciplinas muy diferentes y esto me ha proporcionado conocimientos transversales
inestimables en cuanto a ingeniería del software. Además, todo lo aprendido ha sido plasmado
en esta memoria explicando por qué se han tomado las decisiones de diseño de determinada
forma y no de otra, lo cual puede ser de gran ayuda a otros desarrolladores.
Daniel Ponsoda Montiel
139
14 REFERENCIAS
Amber, Scott W. 2006. The Agile Unified Process. 2006.
Crytek. 2018. CryEngine V Manual. [En línea] 2018.
http://docs.cryengine.com/display/CEMANUAL/CRYENGINE+V+Manual.
Epic Games. 2018. Unreal Engine 4 for Unity developers. [En línea] 2018.
https://docs.unrealengine.com/en-us/GettingStarted/FromUnity.
Epic Games. 2018. Unreal Engine Documentation. [En línea] 2018.
https://docs.unrealengine.com/en-us/.
Forsyth, Tom. 2006. Tom Forsyth Wiki. [En línea] 2006.
http://tomforsyth1000.github.io/blog.wiki.html#%5B%5BScene%20Graphs%20-
%20just%20say%20no%5D%5D.
Khronos Group. 2018. OpenGL wiki. [En línea] 2018.
https://www.khronos.org/opengl/wiki.
Newzoo. 2018. Global games market revenues. [En línea] 2018.
https://newzoo.com/insights/articles/global-games-market-reaches-137-9-billion-in-2018-
mobile-games-take-half/.
Texterity. 2011. Game developer salary survey. [En línea] 2011.
https://www.neogaf.com/threads/game-developer-salary-survey-2011.456723/.
Unity Technologies. 2018. Unity Manual. [En línea] 2018.
https://docs.unity3d.com/Manual/index.html.
Game engine multiplataforma
140
Unity. 2018. Unity3D Official Blog. [En línea] 2018.
https://blogs.unity3d.com/es/2018/03/26/releasing-the-unity-c-source-code/.
Van Verth, James M. y Bishop, Lars M. 2008. Essential Mathematics for Games and
Interactive Applications. 2008.
West, Mick. 2007. Evolve Your Hierarchy. [En línea] 2007.
http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/.
Wikipedia. 2018. List of most expensive video games to develop. [En línea] 2018.
https://en.wikipedia.org/wiki/List_of_most_expensive_video_games_to_develop.
Daniel Ponsoda Montiel
141
15 GLOSARIO
Asset: Término usado para referirnos a un recurso en forma de archivo utilizado por un juego.
Por ejemplo, texturas, materiales, modelos 3D, sprites, secuencias de animaciones, sonidos,
música, scripts, etc.
Acelerómetro: Sensor que mide la aceleración que se le aplica. En los dispositivos móviles
se instala un acelerómetro de 3 ejes y se utiliza aprovechando la aceleración producida por la
gravedad terrestre para determinar su orientación con respecto al horizonte. El problema es
que su precisión no es muy exacta y produce mucho ruido en las lecturas. Esto se corrige en
combinación con un giroscopio.
Billboard: En un espacio 3D, es un plano orientado hacia la cámara, al cual se le aplica una
textura que normalmente representa un sprite, un texto o en general cualquier elemento 2D
que queramos que se muestre sobre la escena 3D.
Blueprints: Sistema visual de programación creado por Epic para su motor Unreal Engine
que permite desarrollar la lógica del juego sin necesidad de escribir código.
Cuaternión: Se trata de un número complejo de cuatro dimensiones. Es decir, es una
estructura matemática con una parte real y tres imaginarias. Se utiliza en los motores gráficos
y de física para aplicar rotaciones a los objetos evitando el indeseado efecto “cardán”, que
aparece al utilizar una matriz de transformación y por el cual perdemos un grado de libertad
cuando varios ejes de rotación están alineados.
Dinámica de cuerpos rígidos: La dinámica es la parte de la ciencia dentro de la física que
estudia el movimiento traslacional y rotacional de los objetos en respuesta a las fuerzas a las
Game engine multiplataforma
142
que están sometidos, como la aceleración producida por la gravedad, o un impacto con otro
objeto. Cuando decimos dinámica de cuerpos rígidos nos referimos a la dinámica de cuerpos
que nunca se van a deformar, con lo cual serán más sencillos de simular.
FlowGraph: Sistema visual de programación creado por CryTek para su motor CryEngine
que permite desarrollar la lógica del juego sin necesidad de escribir código.
Game engine: Conjunto de librerías, herramientas y otros elementos software reutilizables
encaminados a facilitar la tarea de creación y representación de un videojuego. Consta a su
vez de varios subsistemas como el de gráficos, física, colisiones, sonido, matemáticas,
inteligencia artificial, scripting, red, interfaz de usuario, etc.
Giroscopio: Sensor que mide la velocidad angular. Se utiliza en dispositivos móviles para
ayudar a detectar su orientación, pero debido a que sus lecturas son relativas, no podemos
obtener una orientación absoluta a causa del error acumulado. Este error se corrige con la
combinación de un acelerómetro.
Indie: Desarrollador de videojuegos (u otros productos) de cuyos proyectos se sabe que no
dependen ni están respaldados económicamente por una gran empresa del sector. También es
el nombre que reciben los juegos creados por éstos.
Kinemática: Mecánica que determina el movimiento de los cuerpos sin tener en consideración
su masa o las fuerzas a las que está sometido.
LOD: Siglas de Level of Detail. Un objeto 3D puede tener definidos varias mallas que
representan distintos niveles de detalle. La elección de un determinado nivel se hace en tiempo
de ejecución y depende de la proximidad del objeto con el punto de observación. Si el objeto
está muy cerca necesitará un nivel de detalle máximo, mientras que si se encuentra alejado,
bastará con el mínimo nivel de detalle.
Magnetómetro: Sensor que detecta la presencia y dirección de un campo magnético. Se
utiliza en los móviles para detectar la orientación del dispositivo con respecto al eje vertical
aprovechando el campo geomagnético de la tierra.
Daniel Ponsoda Montiel
143
Máquina virtual: Software que simula la estructura de un sistema físico, quedando éste
aislado de la máquina real donde se ejecuta. El sistema simulado puede comportarse como la
CPU de un ordenador real, con registros, pila, unidad de control, memoria, etc. y todos ellos
implementados de forma virtual mediante software. En un game engine lo utilizamos para
embeber scripts en el sistema para manejar la lógica del juego.
Matriz de transformación: Se trata de una estructura matemática, habitualmente de 4x4
elementos y utilizada para aplicar transformaciones de traslación, escala y rotación de forma
simultánea sobre los vectores que definen a los objetos en un espacio 3D.
Motor de juegos: Ver game engine.
Pantalla multitáctil: Se trata de una pantalla que permite detectar la posición de varios
dedos simultáneamente. La entrada es por tanto una lista de vectores 2D con la que se puede
determinar la intención del usuario por medio de gestos, como ampliar o reducir la pantalla
encogiendo dos dedos, o simular el click contextual del ratón haciendo un toque con dos dedos
a la vez.
PhysX: API desarrollado por Nvidia que permite utilizar hardware de aceleración gráfico que
sea compatible con éste para realizar cálculos de física en tiempo real para videojuegos.
Script: En el ámbito de los motores de los videojuegos, lo definiremos como un pequeño
programa escrito en un lenguaje de alto nivel y ejecutado por una máquina virtual que lo
interpreta sin necesidad de compilarlo previamente.
Shader: Fragmento de código que se ejecuta sobre hardware especializado de aceleración de
gráficos con el fin de personalizar la forma en que se muestran los objetos, posibilitando la
aplicación en tiempo real de todo tipo de efectos como sombras, texturas, iluminación, etc.
Sprite: mapa de bits que representa los colores de cada pixel de un elemento gráfico 2D. Se
dibuja en pantalla para representar un personaje o cualquier otro elemento gráfico de un
videojuego.
Textura: Imagen que se aplica sobre la superficie de un objeto 2D o 3D
Game engine multiplataforma
144
Triple A: Rango y término por el cual se conoce a los juegos de última generación que
emplean los últimos avances en tecnología y que han sido desarrollados por grandes empresas
de desarrollo invirtiendo una cantidad considerable de tiempo y recursos humanos.
Daniel Ponsoda Montiel
145
ANEXO I: LIBRERÍA DE MATEMÁTICAS
Para realizar los cálculos que requiere la representación gráfica de un objeto en un espacio
3D, o para el cálculo de las leyes de dinámica de cuerpos rígidos, si disponemos de la suficiente
paciencia, podríamos emplear simplemente álgebra lineal básica, aunque afortunadamente,
con los años han aparecido métodos de cálculo más avanzados que resuelven este tipo de
cuestiones mucho más fácilmente, como son las operaciones algebraicas sobre estructuras de
tipo vector, matrices o cuaterniones.
Este combo de estructuras es la base para cualquier librería de matemáticas de un game
engine. De hecho, el hardware de aceleración de gráficos se ha ido optimizando con el tiempo
para trabajar precisamente con este tipo de cálculos. Incluso, las CPU modernas están también
construidas para optimizar el cálculo vectorial por medio de las extensiones SIMD.
La mayor parte del tiempo de cómputo dentro de los subsistemas de gráficos y física, está
dedicado a cálculos sobre vectores, matrices y cuaterniones. De ahí la importancia de disponer
de una librería perfectamente optimizada y un buen conocimiento sobre la materia para poder
aprovecharla al máximo.
En los siguientes subapartados hablaremos de estas estructuras, su propósito y las
operaciones básicas que se pueden realizar con ellas.
Vectores Un vector es una entidad en un espacio 2D o 3D que posee valores de longitud (o magnitud)
dirección y sentido, pero sin definir una posición. En computación gráfica los vectores se
Game engine multiplataforma
146
utilizan con dos fines: para representar la dirección de un objeto o para determinar un cambio,
por ejemplo, de velocidad o aceleración de este objeto.
Gráficamente lo representamos con una flecha, tal como
se muestra en la figura Figura 0.1. Vectores. Lo que nos
interesa es su dirección, representada por la orientación de
la flecha, el sentido que lo indica la punta de ésta y la
magnitud, ilustrada mediante su longitud. En otras
palabras, dos vectores son iguales si lo son su dirección y
longitud. El origen es importante, ya que es su punto de
aplicación, pero el vector en sí no contiene esta información,
y para poder almacenarla necesitaremos el apoyo de una segunda estructura con las
coordenadas. Dicho de otra forma: dos vectores son iguales si lo son su dirección, sentido y
longitud.
Un vector se representa algebraicamente mediante varios elementos entre paréntesis
separados por coma. Utilizaremos al menos un elemento por dimensión, de forma que, por
ejemplo, para tres dimensiones escribiríamos el vector v así: 𝑣 = (𝑥, 𝑦, 𝑧). No obstante, en la
práctica veremos que se suele usar un cuarto elemento, la w, que se utiliza para facilitar los
cálculos en combinación con matrices. Esto se entenderá mejor en el siguiente apartado. Ahora
pasamos a explicar algunas de las operaciones que pueden realizarse con los vectores.
Propiedades de los vectores
Al igual que los cálculos con cualquier número que conocemos, los vectores tienen ciertas
propiedades algebraicas de las cuales debemos conocer al menos las principales:
1. Conmutativa de la suma: 𝑣 + 𝑤 = 𝑤 + 𝑣
2. Asociativa de la suma: 𝑢 + (𝑣 + 𝑤) = (𝑢 + 𝑣) + 𝑤
3. Elemento neutro de la suma: 𝑣 + 0 = 𝑣
4. Elemento opuesto de la suma: Por cada v, hay un vector -v tal que v+(-v) = 0.
5. Asociativa del producto: (𝑎𝑏)𝑣 = 𝑎(𝑏𝑣)
6. Distributiva: (𝑎 + 𝑏)𝑣 = 𝑎𝑣 + 𝑏𝑣
Figura 0.1. Vectores
Daniel Ponsoda Montiel
147
7. Elemento neutro del producto: 𝑣 · 1 = 𝑣
Magnitud de un vector
La magnitud de un vector v se representa con ||v|| y gráficamente, indica la longitud de
la flecha que representa el vector. Se calcula utilizando el teorema de Pitágoras para la
hipotenusa. Por ejemplo, si tenemos un vector (4,3), consideremos por un momento que el
inicio de la flecha está en el origen de coordenadas. Ahora usando los ejes de coordenadas
podemos formar un triángulo rectángulo con el que podemos calcular la hipotenusa m que
forma el vector tal como se ve en la figura.
Por tanto, en este ejemplo, el cuadrado de la magnitud
del vector según el teorema de Pitágoras sería: 𝑚 = 𝑥 +
𝑦 . De igual modo, para tres dimensiones sería 𝑚 = 𝑥 +
𝑦 + 𝑧 . Y más genéricamente, para los elementos v
pertenecientes al vector en un número d de dimensiones hallaríamos la magnitud con la
siguiente ecuación:
‖𝑣‖ = 𝑣
Normalización de un vector
Normalizar un vector implica convertirlo a unitario, es decir un vector cuya magnitud es
la unidad (1). Esta normalización descarta la información de longitud, pero mantiene su
dirección y sentido. Es muy utilizado para multitud de cálculos que lo requieren de esta forma.
Sabiendo que 𝑥 · = 1 , para normalizar un
vector multiplicaremos cada uno de sus elementos
por la inversa de su magnitud. En el ejemplo
anterior, ‖𝑣‖ = √4 + 3 = 5, por tanto, dividiendo
cada elemento por 5, el vector resultante tendría
Figura 0.2. Magnitud de vector
Game engine multiplataforma
148
longitud unitaria: = 1 y se representaría tal como se ve en la figura marcado en rojo.
Producto escalar
Representado con el operador punto (·), y también conocido como producto Euclídeo
interno o en inglés como dot product, el producto escalar entre dos vectores 𝑣 y 𝑤 que forman
un ángulo 𝜃 entre ellos, da como resultado un escalar y se define por la expresión:
𝑣 · 𝑤 = ‖𝑣‖‖𝑤‖ cos(𝜃)
Si el ángulo que forman los vectores está completamente cerrado, el producto escalar es 1
ya que cos(0) = 1. Si está completamente abierto (180º) será 0, ya que cos ( )=0. Por tanto,
si los vectores están normalizados, el escalar resultante será un número comprendido entre 0
y 1 que indicará el factor de alineación entre los dos vectores. Es decir, si los vectores apuntan
a la misma dirección y sentido, el producto escalar será el valor máximo (1), mientras que si
miran en dirección y sentido completamente opuesto, será el mínimo (0).
Computacionalmente, el cálculo de este producto escalar puede simplificarse utilizando
una propiedad de la Ley de Cosenos, la cual nos permite en este caso evitar calcularlo, ya que
podemos utilizar la siguiente expresión que es equivalente a la anterior:
𝑣 · 𝑤 = 𝑣 𝑤 | 𝑑 = 𝑛ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛𝑒𝑠
𝑃𝑎𝑟𝑎 𝑡𝑟𝑒𝑠 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛𝑒𝑠: 𝑣 · 𝑤 = 𝑣 𝑤 + 𝑣 𝑤 + 𝑣 𝑤
Producto vectorial
Si tenemos dos vectores v y w, podemos calcular un vector u que es ortogonal al plano
formado por los dos primeros. Es decir, un vector cuya dirección corresponde a la orientación
de dicho plano. El producto vectorial entre dos vectores se representa con el operador aspa
(×), y obedece a la siguiente expresión:
‖𝑣 × 𝑤‖ = ‖𝑣‖‖𝑤‖ sen(𝜃)
Figura 0.3. Vector normalizado
Daniel Ponsoda Montiel
149
Como se puede deducir este producto no tiene razón de ser en 2 dimensiones, ya que entre
dos vectores 2D no puede haber un nuevo vector ortogonal, pues esto implicaría una nueva
dimensión.
Otro aspecto importante es que puede haber dos resultados
válidos, y es que la misma dirección puede tener dos sentidos con
respecto a un plano en un espacio tridimensional. La elección de
uno u otro dependerá de si estamos utilizando un sistema de
mano izquierda o de mano derecha. Para aplicar la regla de la
mano derecha, colocando esta mano de forma que el dedo índice
apunte en la dirección del vector v, y el dedo corazón apunte en
la dirección de w, la dirección del vector u será la de nuestro dedo
pulgar. Del mismo modo, la regla de mano izquierda funciona
igual pero utilizando esta mano (el vector resultante tendrá sentido opuesto).
Al igual que con el producto vectorial, aquí también podemos evitar el cálculo de la función
trigonométrica (en este caso el seno). Para vectores en espacio tridimensional resolveríamos
cada componente con las siguientes operaciones:
𝑢 = 𝑣 × 𝑤
𝑢 = 𝑣 𝑤 − 𝑣 𝑤
𝑢 = 𝑣 𝑤 − 𝑣 𝑤
𝑢 = 𝑣 𝑤 − 𝑣 𝑤
Dados los vectores u, v, w y el escalar a, se aplican las siguientes reglas algebraicas (nótese
que el producto vectorial no es conmutativo):
1. 𝑣 × 𝑤 = −𝑤 × 𝑣
2. 𝑢 × (𝑣 + 𝑤) = (𝑢 × 𝑣) + (𝑢 × 𝑤)
3. (𝑢 + 𝑣) × 𝑤 = (𝑢 × 𝑤) + (𝑣 × 𝑤)
4. 𝑎(𝑣 × 𝑤) = (𝑎𝑣) × 𝑤 = 𝑣 × (𝑎𝑤)
5. 𝑣 × 0 = 0 × 𝑣 = 0
6. 𝑣 × 𝑣 = 0
Figura 0.4. Regla de mano derecha
Game engine multiplataforma
150
Puntos Un punto se define con el mismo tipo de estructura que un vector. Sin embargo, entre
estos dos hay ciertas diferencias de concepto que van a determinar el tipo de operaciones que
podemos hacer con ellos.
Un punto representa una localización en el espacio, mientras que un vector, como ya hemos
visto contiene sólo la información sobre dirección, sentido y magnitud. Por tanto, mientras
que este último puede servirnos para determinar el factor de cambio de un objeto con respecto
a un estado anterior, con el punto podemos determinar dicho estado, por ejemplo, su punto
de origen desde el que se desplaza.
Para el ejemplo del cambio de localización, la magnitud del vector podría representar la
velocidad. O bien, para un cambio de velocidad, dicha magnitud representaría la aceleración.
De forma análoga un punto puede localizar un instante concreto en el tiempo, mientras que
un vector puede determinar una duración.
A continuación, citamos las distintas operaciones que pueden realizarse entre puntos y
vectores:
1. La suma de un punto (por ejemplo, una localización) y un vector da como resultado
un punto. Es decir, una nueva localización que depende del punto de origen y el
desplazamiento definido en el vector.
2. La resta entre dos puntos determina:
a. En espacio: una distancia
b. En tiempo: una duración
3. La suma de un vector con otro vector:
a. En espacio: un desplazamiento compuesto
b. En tiempo: una duración compuesta
4. La suma entre dos puntos no tiene sentido ya que sumar dos orígenes tanto en tiempo
como en espacio no da como resultado ninguna información útil.
Daniel Ponsoda Montiel
151
Por último, existe otra diferencia que aparece a la hora de almacenar la información. Tal
como veremos en el apartado de transformaciones con matrices, cuando trabajamos con
vectores o puntos 3D utilizamos 4 elementos (y no 3). Además de las coordenadas
𝑥, 𝑦, 𝑧 cartesianas, añadimos un nuevo componente al que llamamos 𝑤.
Este componente será 0, por ejemplo (x,y,z,0) si la estructura es un vector, o será 1 si se
trata de un punto. Por ejemplo (x,y,z,1). Esto es así para evitar que pueda afectar a un vector
cualquier operación de traslación, ya que como hemos visto esto sería erróneo (un punto más
sumado a un vector no puede dar un vector como resultado). Por ejemplo, no podemos
trasladar un eje de rotación, o una normal. El resultado simplemente no tendría sentido.
Poniendo el componente w a 0 o 1 en función de si se trata de un vector o de un punto
respectivamente, podemos tratar a todos los miembros de un objeto 3D por igual utilizando
la misma matriz de transformación. Todo esto se verá más claramente en el correspondiente
apartado de transformaciones con matrices.
Matrices Una matriz es una estructura bidimensional de números ordenados en m filas y n columnas,
de la cual se dice que es una matriz de 𝑚 × 𝑛 elementos. La matríz se representa con una letra
mayúscula (Por ejemplo, A, B, C). Por notación, un elemento de la matriz se denota con la
misma letra de la matriz en minúscula y se identifica unívocamente por sus coordenadas en el
subíndice, por ejemplo 𝑒 , , donde i es el número de fila y j es el número de columna numeradas
comenzando por 0.
Los siguientes, son ejemplos de matrices:
𝐴 =1 0 00 1 00 0 1
; 𝐵 =
5 24 1
1 45 3
3 08 3
7 21 9
En computación gráfica se hace un uso intensivo de las matrices para aplicar
transformaciones lineales de forma eficiente ya que permiten aplicar varias transformaciones
en una sola operación.
Game engine multiplataforma
152
Matriz identidad
Una matriz con todos sus elementos a 0 excepto en su diagonal donde son 1 se conoce
como matriz identidad (representada con la letra mayúscula I). Se trata del elemento neutro
con respecto a la multiplicación. Es decir, toda matriz M multiplicada por la identidad da
como resultado la propia matriz M.
𝐼 =
1 00 1
⋯ 0… 0
⋮ ⋮0 0
⋱ ⋮… 1
Matriz traspuesta
La traspuesta de una matriz 𝐴 se denota por 𝐴 y es aquella cuyos elementos de A por
columnas están intercambiados por los elementos de A por filas. Por ejemplo:
𝐴 =1 2 34 5 6
; 𝐴 =1 42 53 6
Suma de matrices y multiplicación escalar
Podemos sumar dos matrices que tengan el mismo número de filas m y columnas n. El
resultado será también una matriz de 𝑚 × 𝑛 . Para realizar la operación 𝑆 = 𝐴 + 𝐵 ,
simplemente sumamos cada elemento de la matriz A con el correspondiente elemento de B de
la misma posición. Es decir:
𝑠 , = 𝑎 , + 𝑏 ,
Para multiplicar una matriz A por un escalar s, (operación 𝑃 = 𝑠𝐴) sólo hay que
multiplicar cada elemento de A por dicho escalar:
𝑝 , = 𝑠 · 𝑎 ,
Las propiedades de la suma y la multiplicación escalar son:
1. 𝐴 + 𝐵 = 𝐵 + 𝐴
2. 𝐴 + (𝐵 + 𝐶) = (𝐴 + 𝐵) + 𝐶
3. 𝐴 + 0 = 𝐴
Daniel Ponsoda Montiel
153
4. 𝐴 + (−𝐴) = 0
5. 𝑎(𝐴 + 𝐵) = 𝑎𝐴 + 𝑎𝐵
6. 𝑎(𝑏𝐴) = (𝑎𝑏)𝐴
7. (𝑎 + 𝑏)𝐴 = 𝑎𝐴 + 𝑏𝐴
8. 1𝐴 = 𝐴
Producto de matrices
Es posible multiplicar una matriz A de 𝑚 × 𝑛 elementos por otra B de 𝑝 × 𝑞 elementos si
y sólo si 𝑛 es igual a 𝑝. La matriz resultante será de 𝑚 × 𝑞 elementos.
Si queremos calcular 𝑃 = 𝐴 𝐵, por cada elemento 𝑝 , de la matriz resultado sólo tenemos
que hacer un producto escalar de la siguiente forma:
𝑝 , = 𝑐𝑜𝑙 (𝐴) · 𝑓𝑖𝑙 (𝐵)
Donde 𝑐𝑜𝑙 (𝐴) sería un vector formado por los elementos de la columna i de A, y 𝑓𝑖𝑙 (𝐵)
es un vector formado por los elementos de la fila j de B. En la siguiente figura se muestra
visualmente cómo se seleccionan los pares de vectores que participan en estos productos
escalares para formar la matriz resultado.
También podemos implementarlo de forma algorítmica. Sean 𝐴 y 𝐵 dos matrices con el
tamaño adecuado para poder calcular su producto. Un algoritmo general para resolver 𝑃 =
𝐴 𝐵 sería el que mostramos a continuación:
para i = 0 hasta m-1 para j = 0 hasta p-1 suma = 0
Figura 0.5. Multiplicación de matrices
Game engine multiplataforma
154
para k = 0 hasta n-1 suma = suma + Aik * Bkj fin para Pij = suma fin para fin para
El producto de matrices tiene las siguientes propiedades algebráicas (Nótese que no es
conmutativo):
1. 𝐴𝐵 ≠ 𝐵𝐴
2. 𝐴(𝐵𝐶) = (𝐴𝐵)𝐶
3. 𝑎(𝐵𝐶) = (𝑎𝐵)𝐶
4. 𝐴(𝐵 + 𝐶) = 𝐴𝐵 + 𝐴𝐶
5. (𝐴 + 𝐵)𝐶 = 𝐴𝐶 + 𝐵𝐶
6. (𝐴𝐵) = 𝐵 𝐴
Producto de una matriz por un vector
Como sabemos que para multiplicar dos matrices el número de columnas de la primera
debe ser igual al número de filas de la segunda, podemos considerar un vector como una matriz
de 1 × 𝑛 elementos (vector fila) o bien de 𝑛 × 1 elementos (vector columna). Dependiendo de
nuestra elección la multiplicación será por la izquierda o por la derecha respectivamente.
Producto por la izquierda (vector fila):
𝑣 = [0 1 4]6 2 13 5 90 7 8
Producto por la derecha (vector columna):
𝑣 =6 2 13 5 90 7 8
014
Transformaciones con matrices
Como ya hemos mencionado, una matriz se utiliza principalmente para aplicar
transformaciones lineales sobre los vectores. Ejemplos de estas transformaciones son la escala,
rotación o traslación, que veremos en los siguientes apartados. Una vez tenemos definida la
Daniel Ponsoda Montiel
155
transformación en una matriz, si multiplicamos ésta por un vector, nos dará como resultado
un nuevo vector con la transformación aplicada.
En computación gráfica es muy frecuente aplicar las mismas transformaciones a varios
vectores. Esto se utiliza por ejemplo para aplicar traslación, rotación y escala sobre cada uno
de los vértices de un objeto 3D, los cuales necesitarán las mismas transformaciones para no
deformar el objeto.
Lo que le da la potencia al cálculo con matrices es la posibilidad de acumular varias
transformaciones en una sola para aplicarlas a un vector en una sola operación. Por ejemplo,
si tenemos que aplicar 10 transformaciones 𝑇 , 𝑇 … 𝑇 a un vector v, podemos multiplicar
todas las matrices de transformación entre sí obteniendo una matriz M, de forma que,
multiplicando v por M obtendremos un vector resultado que tendrá aplicadas todas las
transformaciones.
Para transformaciones en un espacio 2D necesitaremos matrices de 3 × 3 elementos,
mientras que para 3D deberán ser de 4 × 4. Esto se verá más claramente en los siguientes
apartados.
Matriz de traslación
La traslación es una de las operaciones más sencillas. Si no utilizáramos matrices,
simplemente consistiría en sumar a cada elemento del vector la distancia que queremos
trasladarlo en sus correspondientes ejes cartesianos.
Para obtener este mismo efecto en un espacio 3D usando matrices, partiendo de la matriz
identidad, y un vector de traslación t, pondremos en la última columna la distancia a trasladar
las coordenadas x,y,z de este vector:
𝑇 =
1 00 1
0 𝑡0 𝑡
0 00 0
1 𝑡0 1
Con esto, para trasladar un vector v utilizando la matriz T, resolvemos el producto 𝑇 · 𝑣:
Game engine multiplataforma
156
1 00 1
0 𝑡0 𝑡
0 00 0
1 𝑡0 1
·
𝑣𝑣𝑣𝑣
=
1𝑣 + 0 + 0 + 𝑡 𝑣0 + 1𝑣 + 0 + 𝑡 𝑣
0 + 0 + 1𝑣 + 𝑡 𝑣0 + 0 + 0 + 1𝑣
=
𝑣 + 𝑡 𝑣𝑣 + 𝑡 𝑣
𝑣 + 𝑡 𝑣𝑣
El componente 𝑣 en este caso estará determinando el factor de influencia de la
transformación. Para aplicar la transformación exactamente como la da el vector de traslación,
el 𝑣 debería ser 1. Como se puede ver, si utilizáramos matrices de 3x3 y vectores de 3
elementos para un espacio 3D no podríamos aplicar traslaciones, por tanto este cuarto
elemento, la 𝑤, es necesario.
Matriz de escala
La escala funciona de forma similar a la traslación. Aplicada sobre los vértices de un objeto
3D lo que hace es expandir sus posiciones con respecto al origen, haciendo efectivamente un
escalado de este objeto.
Para aplicar una escala definida por el vector 𝑠 a nuestro vector 𝑣 construimos la siguiente
matriz:
𝑆 =
𝑠 00 𝑠
0 00 0
0 00 0
𝑠 00 1
Ahora podemos aplicarlo resolviendo el producto 𝑆 · 𝑣 tal como hemos hecho antes para la
traslación:
𝑠 00 𝑠
0 00 0
0 00 0
𝑠 00 1
·
𝑣𝑣𝑣𝑣
=
𝑠 𝑣 + 0 + 0 + 00 + 𝑠 𝑣 + 0 + 0
0 + 0 + 𝑠 𝑣 + 00 + 0 + 0 + 1𝑣
=
𝑠 𝑣𝑠 𝑣𝑠 𝑣𝑣
Vemos que siguiendo la regla de multiplicación de matrices obtenemos un resultado que
es la multiplicación de los respectivos elementos de los vectores 𝑠 y 𝑣 tal como esperábamos.
Matriz de rotación
Con la ayuda de las funciones trigonométricas seno y coseno, podemos crear matrices de
rotación para cada uno de los ejes cartesianos dado un ángulo 𝜃. La rotación se hará en sentido
Daniel Ponsoda Montiel
157
antihorario en un sistema de coordenadas de mano derecha. Estas matrices son las que
mostramos a continuación. Nótese que la cuarta columna y fila las mantenemos para poder
combinar estas matrices con otras matrices de transformación, pero en realidad no se utilizan
para la rotación:
𝑅 (𝜃) =
1 00 𝑐𝑜𝑠(𝜃)
0 0−𝑠𝑒𝑛(𝜃) 0
0 𝑠𝑒𝑛(𝜃)0 0
cos(𝜃) 00 1
𝑅 (𝜃) =
cos (𝜃) 0 0 1
𝑠𝑒𝑛(𝜃) 00 0
−𝑠𝑒𝑛(𝜃) 00 0
cos(𝜃) 00 1
𝑅 (𝜃) =
cos (𝜃) −𝑠𝑒𝑛(𝜃)
𝑠𝑒𝑛(𝜃) cos(𝜃)0 00 0
0 00 0
1 00 1
Cada una de las rotaciones sobre los
ejes cartesianos x,y,z recibe el nombre en
inglés de roll, pitch y yaw respectivamente.
O bien en español, rotación longitudinal,
trasversal y vertical, tal como se muestra
en la figura.
Como ya se ha comentado, al igual que
cualquier otra transformación, podemos
combinar varias de estas rotaciones
multiplicando estas matrices para formar
una matriz de rotación compuesta. Y también, como sucede al combinar con otras
transformaciones, el orden de la multiplicación determina distintos resultados. Por ejemplo,
como puede comprobarse experimentalmente, no es lo mismo girar un objeto 30 grados sobre
el eje x, y luego 40 grados sobre el eje y, que hacerlo en orden inverso. La orientación resultante
no será la misma. Del mismo modo obtendremos distintos resultados, por ejemplo, entre
Figura 0.6. Ejes de rotación
Game engine multiplataforma
158
traslación y rotación según su orden de aplicación. No significa que un determinado orden sea
incorrecto, sino que irá en función de nuestras necesidades.
También conocemos una matriz de rotación especial que nos permite rotar un objeto en
un ángulo 𝜃 sobre un eje arbitrario determinado por el vector u y que definimos así:
𝑅(𝑢, 𝜃) =
=
⎣⎢⎢⎢⎡
cos 𝜃 + 𝑢 (1 − cos 𝜃) 𝑢 𝑢 (1 − cos 𝜃) − 𝑢 sen 𝜃
𝑢 𝑢 (1 − cos 𝜃) + 𝑢 sen 𝜃 cos 𝜃 + 𝑢 (1 − cos 𝜃)
𝑢 𝑢 (1 − cos 𝜃) + 𝑢 sen 𝜃 0
𝑢 𝑢 (1 − cos 𝜃) − 𝑢 sen 𝜃 0
𝑢 𝑢 (1 − cos 𝜃) − 𝑢 sen 𝜃 𝑢 𝑢 (1 − cos 𝜃) + 𝑢 sen 𝜃
0 0 cos 𝜃 + 𝑢 (1 − cos 𝜃) 0
0 1⎦⎥⎥⎥⎤
Cofactor de un elemento de la matriz
El cofactor del elemento 𝑎 de la matriz A se calcula con la siguiente expresión:
𝐶 , = (−1) 𝐴 ,
Donde 𝐴 , es la submatriz resultante de eliminar la fila i y la columna j de la matriz A.
Determinante de una matriz
El determinante de una matriz cuadrada A de cualquier tamaño es un escalar denotado
por |𝐴| y podemos calcularlo eligiendo cualquier fila i de la matriz y evaluando la siguiente
expresión:
|𝐴| = 𝑎 , 𝐶 ,
Donde 𝐶 , es el cofactor 𝑎 , . Por tanto, esta expresión es recursiva, ya que el método para
hallar el cofactor que hemos visto requiere a su vez del determinante. Pero afortunadamente,
si tenemos una matriz de tamaño conocido (lo más habitual son matrices de 3x3 o 4x4),
podemos escribir todo el código necesario sin tener que hacer uso de la recursividad.
Matriz adjunta
La matriz adjunta de A se denota por adj(𝐴) y es el resultado de sustituir cada uno de los
elementos 𝑎 de A por su correspondiente cofactor.
Daniel Ponsoda Montiel
159
Matriz inversa
Una matriz inversa de A se denota por 𝐴 y cumple que 𝐴 · 𝐴 = 𝐴 · 𝐴 = 𝐼. Es decir,
toda matriz multiplicada por su inversa da como resultado la matriz identidad.
La operación de división no existe para las matrices. Por tanto, si queremos dividir una
matriz A por otra matriz B lo haremos multiplicando por la inversa de B, que denotamos
como 𝐵 .
Para calcular la inversa de una matriz, multiplicamos la traspuesta de su adjunta por el
inverso de su determinante tal como muestra la siguiente expresión:
𝐴 =1
|𝐴|adj(𝐴)
Uno de los principales usos que se le da a esta matriz en computación gráfica es para
calcular la matriz transformación para la “vista” a partir de la cámara (punto de observación)
de una escena 3D.
Propiedades de la matriz inversa:
1. 𝐴 · 𝐴 = 𝐼
2. 𝐴 · 𝐴 = 𝐼
3. (𝐴 ) = 𝐴
4. No siempre existe una inversa para una matriz
Cuaterniones Desarrollado por Sir William Hamilton, e introducido posteriormente por Ken Shoemake
en 1980 para la computación gráfica, un cuaternión es un número complejo de cuatro
dimensiones donde tenemos una parte real y tres imaginarias. Por ejemplo:
𝑞 = 𝑤 + 𝑥𝑖 + 𝑦𝑗 + 𝑧𝑘
También se suele representar como un vector, obviando los coeficientes imaginarios i,j,k:
Game engine multiplataforma
160
𝑞 = (𝑤, 𝑥, 𝑦, 𝑧)
El cuaternión, utilizado como alternativa a las matrices de rotación, permite evitar el
indeseado efecto “gimbal lock”, por el cual perdemos un grado de libertad cuando varios ejes
de rotación están alineados. Este efecto se puede apreciar mejor en la figura
El gimbal es un conjunto de aros concéntricos que permiten sostener una figura en el
centro de forma que podemos orientarla con respecto a tres ejes. Estando en la posición A,
tenemos 100% de libertad para cualquier giro. Sin embargo, en la posición B, con los 3 ejes
alineados, si giramos el aro exterior (verde) o el interior (rojo) el efecto es el mismo, con lo
cual hemos perdido un grado de libertad. Este es precisamente el problema que resuelve el
cuaternión.
Cuaternión a partir de un eje de rotación y un ángulo
Dado un eje de rotación definido por el vector 𝑣 , y un ángulo 𝜃 , el cuaternión que
representa esta transformación se calcula de la siguiente forma:
𝑞 = (cos , 𝑣 sen , 𝑣 sen , 𝑣 sen )
Además, podemos rotar una orientación inicial representada en un cuaternión,
multiplicándolo por otro que contenga la variación a aplicar (ver Operaciones con
cuaterniones).
Figura 0.7. Demostración del efecto gimbal lock
Daniel Ponsoda Montiel
161
Operaciones con cuaterniones
Podemos hacer la suma de dos cuaterniones término a término, igual que lo haríamos con
un vector o con un número complejo cualquiera.
𝑎 + 𝑏 = (𝑎 + 𝑏 ) + (𝑎 + 𝑏 )𝑖 + 𝑎 + 𝑏 𝑗 + (𝑎 + 𝑏 )𝑘
El producto se realiza de la siguiente forma (Nótese que al igual que las matrices, no es
conmutativo):
𝑎𝑏 = 𝑎 𝑏 − 𝑎 𝑏 − 𝑎 𝑏 − 𝑎 𝑏 + 𝑎 𝑏 + 𝑎 𝑏 + 𝑎 𝑏 − 𝑎 𝑏 𝑖
+ 𝑎 𝑏 − 𝑎 𝑏 + 𝑎 𝑏 + 𝑎 𝑏 𝑗 + 𝑎 𝑏 + 𝑎 𝑏 − 𝑎 𝑏 + 𝑎 𝑏 𝑘
Daniel Ponsoda Montiel
163
ANEXO II. SISTEMA DE GRÁFICOS
El sistema de gráficos se encarga de dibujar en pantalla los elementos de una escena que
se encuentra en un espacio 2D o 3D.
Debe estar perfectamente optimizado ya que es una parte crítica del programa que se
ejecuta varias de veces por segundo. La mayor parte de la eficiencia dependerá de lo
optimizada que esté la librería de matemáticas de la que depende (ver apartado ¡Error! No
se encuentra el origen de la referencia.). En este apartado haremos una introducción a
los conceptos fundamentales para poder entender el funcionamiento de un motor de gráficos
3D.
Pipeline de procesamiento de gráficos En cualquier aplicación que necesite dibujar gráficos en tiempo real, como puede ser un
videojuego, vamos a realizar una serie de pasos para cada fotograma cuya salida dependerá
del paso anterior. Por tanto, se deben realizar en un orden concreto. De ahí su nombre de
pipeline (tubería) de renderizado.
Los pasos más importantes del API de OpenGL se muestran en el siguiente diagrama
(Khronos Group, 2018), donde los marcados en naranja son opcionales:
Game engine multiplataforma
164
Todos los pasos mostrados (excepto el de rasterización) son programables mediante
shaders. Los shaders son programas que se ejecutan en un hardware de aceleración de gráficos
compatible.
Vertex shader
La lista de vértices que se pasa al vertex shader puede contener listas con información
sobre posición, color, normales, etc. Cuando se obtiene la lista de vértices, el shader
típicamente se programa para aplicar las transformaciones para cada uno utilizando la matriz
MVP (ver apartado 0 Matriz Modelo-Vista-Proyección) y también puede aplicar efectos de
iluminación de tipo vértice, como el que se consigue con la técnica Gouraud.
Teselado
La salida de vértices se pasa opcionalmente al shader de teselado que puede, a partir de
parches definidos de las primitivas que forman estos vértices, crear nuevas primitivas más
pequeñas subdividiendo las originales.
Lista ordenada de vértices (VAO, VBO) con sus atributos
Vertex shader
Teselado
Geometry shader
Rasterización
Fragment shader
Vértices transformados
Vértices + primitivas adicionales
Secuencia de primitivas
Info color + coordenadas destino
Píxeles finales en pantalla
Daniel Ponsoda Montiel
165
Geometry shader
Un geometry shader toma como entrada una primitiva y puede dar como salida cero o
varias primitivas. Se suele utilizar para definir algoritmos de LOD (Level-of-detail) que dan
como salida geometrías cuya complejidad depende de la proximidad del observador.
Rasterización
En este paso, teniendo toda la información vectorial de la escena, se hace corresponder
cada fragmento de esta a pixeles discretos donde se situarán éstos.
Vertex shader
Por último, por cada pixel, aplicaremos el algoritmo deseado para determinar el color final
en función del efecto que queramos aplicar. En esta fase se puede aplicar cualquier efecto que
necesite ser procesado pixel a pixel, como sombreado Phong o distintos efectos de post-
procesado como motion-blur, DOF (Depth-of-field), scan-lines, etc.
La cámara La cámara es el punto de observación para una escena 3D y se define por medio de una
matriz C de transformación que contiene simplemente un punto (para la localización) y tres
vectores (para la orientación).
Una operación típica que se realiza
para calcular la orientación de la cámara
es la función lookAt, que consiste en hacer
que se oriente hacia un punto arbitrario de
la escena. Al punto de origen de la cámara
lo llamamos p, y al punto al que queremos
mirar lo llamamos t.
Lo que queremos construir es una
matriz C que indique la posición y
orientación de la cámara. Para la Figura 0.1. Matriz cámara
Game engine multiplataforma
166
orientación necesitamos los vectores arriba, derecha y frente, y para la posición p, un punto
de 3 coordenadas. La distribución en la matriz de esta información será la siguiente:
𝐶 =
𝑑𝑒𝑟𝑒𝑐ℎ𝑎 𝑎𝑟𝑟𝑖𝑏𝑎𝑑𝑒𝑟𝑒𝑐ℎ𝑎 𝑎𝑟𝑟𝑖𝑏𝑎
𝑓𝑟𝑒𝑛𝑡𝑒 𝑝𝑓𝑟𝑒𝑛𝑡𝑒 𝑝
𝑑𝑒𝑟𝑒𝑐ℎ𝑎 𝑎𝑟𝑟𝑖𝑏𝑎0 0
𝑓𝑟𝑒𝑛𝑡𝑒 𝑝0 1
El vector frente es sencillo de calcular. Simplemente hallamos la normal de la diferencia
entre t y p:
𝑓𝑟𝑒𝑛𝑡𝑒 = 𝑛𝑜𝑟𝑚𝑎𝑙(𝑡 − 𝑝)
Ahora ya tenemos la dirección hacia la que apuntar, pero sólo con esto aún no sabemos lo
que es arriba y abajo. Sin ninguna referencia todavía, tendremos que definir temporalmente
un vector que nos permita crear un plano con el de frente de tal forma que su normal apunte
justo a la derecha. Esto nos lo garantiza el vector unitario 𝑦: (0,1,0), ya que podemos obtener
el vector derecha mediante el siguiente producto vectorial:
𝑑𝑒𝑟𝑒𝑐ℎ𝑎 = 𝑦 × 𝑓𝑟𝑒𝑛𝑡𝑒
Finalmente, para saber cuál es la dirección real hacia arriba de la cámara sólo nos queda
calcular el producto vectorial entre frente y derecha:
𝑎𝑟𝑟𝑖𝑏𝑎 = 𝑓𝑟𝑒𝑛𝑡𝑒 × 𝑑𝑒𝑟𝑒𝑐ℎ𝑎
Matriz Modelo-Vista-Proyección En un espacio 3D trabajamos constantemente con conjuntos de puntos (vértices) que
forman objetos más complejos en la escena. Sin embargo, la pantalla que nos permite
visualizarlos es un plano de dos dimensiones. Además, debemos poder determinar en qué
posición dibujar cada objeto en función de la posición del observador virtual en la escena.
Todos estos problemas se resuelven utilizando una matriz compuesta de 3 transformaciones
que conocemos como matriz MVP (o modelo-vista-proyección).
Daniel Ponsoda Montiel
167
Matriz modelo
Cada objeto individual que podemos representar está
definido por un conjunto de puntos cuya posición es relativa
al origen de coordenadas propio de dicho objeto. La matriz
modelo (M) determina cómo se deben situar estos puntos
desde el espacio modelo (objeto) al espacio mundo (escena).
Esta matriz está compuesta a su vez de las típicas
transformaciones traslación, rotación, escala.
Como vemos en la figura, el origen de coordenadas relativo
para el objeto seguirá siendo siempre el original, y cualquier
modificación que hagamos sobre sus puntos individuales
afectará en relación a este espacio.
Matriz vista
Una parte fundamental de la visualización de una escena 3D es saber cómo serán vistos
los objetos (su localización) relativos al punto de observación, es decir, desde la cámara de la
escena.
Para ello, lo que hacemos es aplicar una transformación a cada uno de los objetos de la
escena para que se alineen de forma adecuada con la cámara. Esto significa transformarlos
desde el espacio “mundo” al espacio “vista”.
Esta transformación se almacena en la matriz vista (V) y se calcula mediante la inversa
de la matriz de la cámara (ver el apartado 0 La cámara).
𝑉 = 𝐶
Figura 0.2. Transformación con la matriz modelo
Game engine multiplataforma
168
Matriz proyección
Para poder simular la forma en que los humanos vemos un espacio tridimensional sobre
una pantalla lo que hacemos es calcular la proyección de cada punto a dibujar sobre el plano
imaginario formado por dicha pantalla.
Siendo c el punto de origen de la cámara,
cada punto v a proyectar dará como
resultado un punto p sobre el espacio
proyectado tal como se muestra en la figura.
Además, establecemos una distancia n (near)
que indica el punto de corte más cercano de
nuestra área visible (y es donde se proyectará
todo), y una distancia f (far) que indica el
punto de corte más lejano a partir del cual no veremos nada.
Como vamos a proyectar sobre un espacio 2D que puede no ser cuadrado, sino que
típicamente podrá ser panorámico 16:9, o tal vez 4:3, deberemos calcular un factor de
corrección para esta relación de aspecto 𝑎. Esto podemos calcularlo a partir de las dimensiones
de la pantalla, siendo w el ancho y h el alto: 𝑎 =
Las coordenadas de p se calculan de la siguiente forma:
𝑝 =𝑛𝑣
−𝑎𝑣 ; 𝑝 =
𝑛𝑣
−𝑣 ; 𝑝 = −𝑑
Finalmente, podemos llevar estos cálculos a una matriz de proyección que quedará así:
𝑃 =
⎣⎢⎢⎢⎢⎡𝑛
𝑎0
0 𝑛
0 0 0 0
0 00 0
𝑛 + 𝑓
𝑛 − 𝑓
2𝑛𝑓
𝑛 − 𝑓−1 0 ⎦
⎥⎥⎥⎥⎤
Figura 0.3. Proyección de un punto sobre un plano
Daniel Ponsoda Montiel
169
ANEXO III: MANUAL DEL API
Este anexo es un manual de referencia del interfaz expuesto por el motor al entorno de
programación Lua. Las funciones descritas aparecen clasificadas por el objeto al que pertenecen
y por orden alfabético. El manual está pensado para utilizarse como libro de consulta. En
ningún caso es necesario leerlo por completo para poder empezar a utilizar el motor. Para ello,
se recomienda al lector remitirse al capítulo “Ejemplos de uso” donde encontrará una guía de
iniciación más adecuada en las primeras fases de aprendizaje
AssetMapper AssetMapper.forceReload Descripción: Propiedad que indica si se debe forzar el recargado de ficheros.
Cuando está deshabilitado los ficheros de assets se obtienen de la caché si ya han sido accedidos previamente. Sin embargo, cuando estamos desarrollando, puede interesarnos forzar la recarga si vamos a cambiar ficheros mientras hacemos nuestras pruebas.
Ver también: forceReload
AssetMapper:getMesh(name) Descripción: Obtiene una malla 3D desde disco. Si el recurso fue utilizado
anteriormente lo obtendrá a partir de la caché. El archivo de malla debe estar en formato obj. Si utilizamos Blender podemos exportar nuestra escena a ese formato utilizando la opción de Export del menú File. Téngase en cuenta que sólo se cargarán las mallas de objetos. El sistema no reconoce cámaras, luces o cualquier otro objeto no renderizable.
Parámetros: - name: Cadena con el nombre de archivo que contiene la malla
Devuelve: Instancia de un objeto tipo Mesh
Ver también: forceReload
Game engine multiplataforma
170
AssetMapper:getShader(name) Descripción: Obtiene un shader desde disco que deberá ser un archivo con el
código escrito lenguaje glsl. Si el recurso fue utilizado anteriormente lo obtendrá a partir de la caché.
Parámetros: - name: Cadena con el nombre de archivo que contiene el shader
Devuelve: Instancia de un objeto tipo Shader
Ver también: forceReload
Daniel Ponsoda Montiel
171
CameraComponent Camera.clearColor Descripción: Propiedad de tipo Vector4 que permite modificar el color de borrado
de la ventana de gráficos
Ver también: CameraComponent.lookAt(eye,center,up) Descripción: Establece una matriz de transformación para la cámara de tal forma
que la entidad esté en la posición indicada por “eye”, mirando hacia “center” y tomando “up” como vector que apunta en dirección y sentido del el eje vertical natural hacia arriba.
Parámetros: - eye: Vector3 que indica la posición de la cámara - center: Vector3 que indica la posición objetivo hacia donde
queremos que se oriente - up: Vector3 unitario que indica la dirección y sentido del eje
vertical hacia arriba Devuelve:
Ver también: CameraComponent.setPerspectiveProjection, CameraComponent.setOrthogonalProjection
CameraComponent:setOrthogonalProjection(left,right,bottom,top,near,far) Descripción: Establece la matriz de proyección de la cámara en modo ortogonal.
Es decir, de tal forma que los objetos se muestran en pantalla del mismo tamaño sin importar la distancia a la cámara.
Parámetros: - left,right: Escalares que especifican las posiciones de los planos de corte izquierdo y derecho
- bottom,top: Escalares que especifican las posiciones de los planos de corte inferior y superior
- near,far: Escalares que especifican las posiciones de los planos de corte más cercano y lejano
Devuelve:
Ver también: CameraComponent.setPerspectiveProjection, CameraComponent.LookAt
CameraComponent:setPerspectiveProjection(fov,aspect,near,far)
Game engine multiplataforma
172
Descripción: Establece la matriz de proyección de la cámara en modo perspectiva. Es decir, de tal forma que los objetos se muestran más pequeños a medida que se alejan de la cámara.
Parámetros: - fov: Escalar que indica el ángulo del campo de visión en radianes - aspect: Escalar que indica la relación de aspecto entre
ancho/alto del viewport (ventana de visión) - near,far: Escalares que especifican las posiciones de los planos
de corte más cercano y lejano Devuelve:
Ver también: CameraComponent.setOrthogonalProjection, CameraComponent.LookAt
Camera.viewPort Descripción: Propiedad de tipo Vector4 que permite modificar la posición y
tamaño del viewport. Las primeras dos coordenadas del vector hacen referencia a la posición x,y, y las dos últimas al tamaño en ancho y alto.
Ver también: CameraComponent.setOrthogonalProjection, CameraComponent.setPerspectiveProjection, CameraComponent.LookAt
Daniel Ponsoda Montiel
173
DrawableComponent DrawableComponent.material Descripción: Propiedad que da acceso a la instancia del material que utiliza el
componente de gráficos de la entidad.
Ver también: Material, Entity DrawableComponent:setMesh(mesh) Descripción: Asigna una malla 3D al componente de gráficos de la entidad. Nótese
que esta malla será diferente a la del componente de física. Por tanto podemos usar en los gráficos una versión de la malla más detallada.
Parámetros: - mesh: Malla 3D a asignar
Devuelve:
Ver también: Mesh, Shape, PhysicsComponent DrawableComponent:setShape(shape) Descripción: Asigna una forma al componente gráfico a partir de un volumen
primitivo.
Parámetros: - shape: Volumen primitivo a asignar
Devuelve:
Ver también: Mesh, Shape, PhysicsComponent, BoxVolume, ConeVolume, CylinderVolume, CapsuleVolume, SphereVolume
Game engine multiplataforma
174
Entity Entity:addComponent(component) Descripción: Añade un componente a la entidad que podrá ser cualquier objeto
derivado de la clase Component. Una entidad sólo puede contener un componente por cada tipo. Por ejemplo, no es posible añadir dos GraphicsComponent o dos PhysicsComponent a la entidad. Sin embargo, en el caso de los scripts, podemos añadir un ScriptComponent que actúa como contenedor de instancias de script, con lo cual podemos combinar, en una misma entidad, lógicas procedentes de varios archivos.
Parámetros: - component: Instancia de un objeto derivado de la clase Component que vamos a añadir a la entidad
Devuelve:
Ver también: Component
Entity.camera Descripción: Propiedad que da acceso al componente CameraComponent de la
entidad. Es la propiedad que debemos usar por ejemplo para la cámara global de la escena si queremos manipularla.
Ver también: CameraComponent, GraphicsComponent
Entity.drawable Descripción: Propiedad que da acceso al componente DrawableComponent
derivado de GraphicsComponent.
Ver también: DrawableComponent, GraphicsComponent.
Entity:getComponent(componentName) Descripción: Obtiene un componente de la entidad a partir de su nombre de tipo.
Parámetros: - componentName: Nombre del tipo de componente que vamos a obtener. Las opciones son: “graphics”, “physics”, “transform”, o “script”.
Devuelve:
Ver también: Component
Entity.id
Daniel Ponsoda Montiel
175
Descripción: Propiedad que contiene el identificador único de la entidad en la escena. Esta propiedad sólo es accesible para lectura y se establece en el constructor del objeto
Ver también: Entity.new(id)
Entity.new(id) Descripción: Constructor del objeto Entity que representa una entidad en la
escena.
Parámetros: - id: Cadena con el identificador único de la entidad
Devuelve: Instancia de un objeto tipo Entity
Ver también:
Entity.physics Descripción: Propiedad que da acceso al componente PhysicsComponent de la
entidad.
Ver también: PhysicsComponent Entity:removeComponent(componentName) Descripción: Elimina un componente de una entidad por nombre de tipo. Nótese
que si eliminamos el componente “script” se eliminarán de la entidad todas las instancias de script relacionadas.
Parámetros: - componentName: Nombre del tipo de componente que vamos a eliminar. Las opciones son: “graphics”, “physics”, “transform”, o “script”.
Devuelve: Verdadero en caso de que se haya eliminado el componente
Ver también: Component
Entity.script Descripción: Propiedad que da acceso al componente ScriptComponent de la
entidad en caso de tenerlo asociado.
Ver también: ScriptComponent
Entity.transform Descripción: Propiedad que da acceso al componente transform de la entidad en
caso de tenerlo asociado.
Ver también: TransformComponent
Game engine multiplataforma
176
ion (alias de SystemManager) ion:addEntity(id,shape,transform=nil) Descripción: Función fachada que crea y añade a la escena una entidad con los
parámetros indicados. El resto de opciones básicas se establecen por defecto.
Parámetros: - id: Cadena que contiene el identificador único de la entidad a crear.
- shape: Objeto de tipo Shape o Mesh que establece la forma de la entidad para el componente de gráficos.
- transform: Objeto de tipo TransformComponent que determina las transformaciones afines iniciales de la entidad
Devuelve: Instancia de objeto de tipo Entity
Ver también: ion:removeEntity, ion:getEntity, ion:addPhysicsEntity, Position, PositionRotation, TransoformComponent
ion:addPhysicsEntity(id,shape,flags,transform=nil) Descripción: Función fachada que crea y añade a la escena una entidad con los
parámetros indicados y con componente de física. El resto de opciones básicas se establecen por defecto.
Parámetros: - id: Cadena que contiene el identificador único de la entidad a crear.
- shape: Objeto de tipo Shape o Mesh que establece la forma de la entidad para el componente de gráficos.
- flags: Opciones combinables que indican el tipo de física a aplicar. Pueden ser: KINEMATIC, DYNAMIC, GHOST, TRIGGER.
- transform: Objeto de tipo TransformComponent que determina las transformaciones afines iniciales de la entidad
Devuelve: Instancia de objeto de tipo Entity
Ver también: ion:removeEntity, ion:getEntity, ion:addEntity, Position, PositionRotation, TransoformComponent
ion:getEntity(id) Descripción: Recupera la entidad de la escena cuyo identificador se especifica por
parámetro
Parámetros: - id: Cadena que contiene el identificador de la entidad a obtener
Daniel Ponsoda Montiel
177
Devuelve: Instancia de la entidad recuperada o nil si no existe
Ver también: ion:removeEntity, ion:addEntity
ion:removeEntity(id) Descripción: Elimina la entidad de la escena cuyo identificador se especifica por
parámetro
Parámetros: - id: Cadena que contiene el identificador de la entidad a eliminar
Devuelve:
Ver también: ion:getEntity, ion:addEntity
ion:reset() Descripción: Elimina todas las entidades de la escena y reinicia los sistemas ECS
Parámetros:
Devuelve:
Ver también:
ion:run(luaFileName) Descripción: Ejecuta el script del archivo cuyo nombre se indica por parámetro.
Esta función está pensada para utilizarse manualmente desde la consola con el fin de realizar pruebas. No se aconseja su uso dentro de otro script. Para ejecutar código dentro de nuestro código se preferirá siempre el uso de funciones.
Parámetros: - id: Cadena que contiene el identificador de la entidad a eliminar
Devuelve: El valor devuelto es nil, pero podremos ver el resultado de la ejecución en la consola de script.
Ver también:
ion.showFPS Descripción: Propiedad que establece si se deben mostrar los fotogramas por
segundo en la consola de depuración. En caso de estar habilitado, se imprimirá un log cada segundo indicando la media de fps obtenida en ese intervalo. Nótese que los FPS no aparecerán en la consola de script, sino en la ventana de depuración del entorno de desarrollo (Por ejemplo el log de AndroidStudio o la ventana de Salida de Visual Studio)
Ver también:
Game engine multiplataforma
178
Material Material.diffuse Descripción: Propiedad que nos permite leer/escribir el atributo “diffuseColor”
del shader. Por ejemplo, Material.diffuse = color, es equivalente a llamar a Entity:setVector4(“diffuseColor”, color)
Ver también: Shader, setVector4, getVector4, setFloat, getFloat
Material:getFloat(key) Descripción: Obtiene el valor del atributo uniforme de tipo float del shader con
el nombre solicitado.
Parámetros: - key: Nombre del atributo uniforme del shader a obtener.
Devuelve: Valor del atributo obtenido
Ver también: Shader, setVector4, getVector4, getFloat
Material:getVector4(key) Descripción: Obtiene el valor del atributo uniforme de tipo Vector4 del shader
con el nombre solicitado.
Parámetros: - key: Nombre del atributo uniforme del shader a obtener.
Devuelve: Valor del atributo obtenido
Ver también: Shader, setVector4, setFloat, getFloat Material:setFloat(key,value) Descripción: Establece el valor del atributo uniforme de tipo float del shader con
el nombre indicado.
Parámetros: - key: Nombre del atributo uniforme del shader a modificar. - value: Nuevo valor del atributo
Devuelve:
Ver también: Shader, setVector4, getVector4, setFloat, getFloat
Material:setShader(shader) Descripción: Establece el shader que se aplicará al material para programar el
efecto deseado.
Parámetros: - shader: Objeto de tipo Shader
Devuelve:
Ver también: Shader, AssetMapper:getShader
Daniel Ponsoda Montiel
179
Material:setVector4(key,value) Descripción: Establece el valor del atributo uniforme de tipo Vector4 del shader
con el nombre indicado.
Parámetros: - key: Nombre del atributo uniforme del shader a modificar. - value: Nuevo valor del atributo
Devuelve:
Ver también: Shader, setVector4, getVector4, setFloat, getFloat
Game engine multiplataforma
180
PhysicsComponent PhysicsComponent.angularVelocity Descripción: Propiedad de tipo Vector3 que permite obtener o modificar la
velocidad angular del objeto usando ángulos Euler.
Ver también: linearVelocity PhysicsComponent:applyCentralImpulse(impulse) Descripción: Aplica un impulso al objeto desde su centro de masas dando como
resultando que se mueva en la dirección indicada si las demás condiciones físicas lo permiten
Parámetros: - impulse: Vector3 que indica la dirección del impulso.
Devuelve:
Ver también: mass, gravity PhysicsComponent.gravity Descripción: Propiedad de tipo Vector3 que da acceso a la dirección de la
gravedad local para el objeto. En realidad, se refiere a cómo afecta la gravedad a esta entidad. Para modificar la dirección de la gravedad global que afecta a todos los objetos, usaremos Physics.setGravity
Ver también: gravity PhysicsComponent.linearVelocity Descripción: Propiedad de tipo Vector3 que permite obtener o modificar la
velocidad lineal del objeto indicando la velocidad en los 3 ejes cartesianos.
Ver también: linearVelocity PhysicsComponent.mass Descripción: Propiedad escalar que da acceso a la masa del objeto. Por defecto es
1.0. Un objeto con masa 0.0 se considera de masa infinita en el sentido de que no se moverá al colisionar. Además, tampoco será afectado por la gravedad.
Ver también: gravity
Daniel Ponsoda Montiel
181
PhysicsComponent.new(shape) Descripción: Constructor del componente tipo PhysicsComponent
Parámetros: - shape: Volumen o malla 3D a asignar al componente y que se utilizará para la detección de colisiones.
Devuelve: Instancia de objeto PhysicsComponent
Ver también:
Game engine multiplataforma
182
Random Random:float(min=0,max=RAND_MAX) Descripción: Obtiene un número aleatorio de tipo coma flotante entre un rango
Parámetros: - min: Valor mínimo del rango. Si no se indica es cero. - max: Valor máximo del rango. Si no se indica es el máximo valor
para un numero de tipo coma flotante Devuelve:
Ver también: Random.seed, Random.int Random:int(min=0,max=RAND_MAX) Descripción: Obtiene un número aleatorio de tipo entero entre un rango
Parámetros: - min: Valor mínimo del rango. Si no se indica es cero. - max: Valor máximo del rango. Si no se indica es el máximo valor
para un numero de tipo entero Devuelve:
Ver también: Random.seed, Random.float Random:seed(s) Descripción: Establece la semilla para la secuencia de números pseudo-aleatorios
Parámetros: - s: Valor para usar como semilla Devuelve:
Ver también: Random.unit, Random.float, Random.int Random:unit() Descripción: Obtiene un número aleatorio de tipo coma flotante unitario (entre
0.0 y 1.0)
Parámetros:
Devuelve:
Ver también: Random.seed, Random.float, Random.int
Daniel Ponsoda Montiel
183
Shapes BoxVolume(x,y,z) Descripción: Factoría de un objeto que define un volumen en forma de caja
Parámetros: - x: Tamaño de la caja sobre el eje local X - y: Tamaño de la caja sobre el eje local Y - z: Tamaño de la caja sobre el eje local Z
Devuelve: Instancia de un objeto Box
Ver también: CapsuleVolume(radius,height) Descripción: Factoría de un objeto que define un volumen en forma de cápsula
Parámetros: - radius: Radio de la cápsula formado sobre el eje Y - height: Altura de la cápsula sobre el eje Y
Devuelve: Instancia de un objeto Capsule
Ver también: CylinderVolume CylinderVolume(radius,height) Descripción: Factoría de un objeto que define un volumen en forma de cilindro
Parámetros: - radius: Radio del cilindro formado sobre el eje Y - height: Altura del cilindro sobre el eje Y
Devuelve: Instancia de un objeto Cylinder
Ver también: CapsuleVolume SphereVolume(radius) Descripción: Factoría de un objeto que define un volumen en forma de esfera
Parámetros: - radius: Radio de la esfera Devuelve: Instancia de un objeto Sphere
Ver también:
Game engine multiplataforma
184
TransformComponent TransformComponent.eulerAngles Descripción: Propiedad de tipo Vector3 que obtiene o modifica la posición en la
matriz de transformación.
Ver también: scale, position TransformComponent:getRotationAngle() Descripción: Obtiene el ángulo en el que está rotada la transformación atendiendo
al modo de rotación eje-ángulo
Parámetros:
Devuelve: Escalar que representa el ángulo de rotación
Ver también: eulerAngles, setAxisAngle, getRotationAxis TransformComponent:getRotationAxis() Descripción: Obtiene el eje de rotación sobre el que está rotada la matriz
atendiendo al modo de rotación eje-ángulo
Parámetros:
Devuelve: Escalar que representa el ángulo de rotación
Ver también: eulerAngles, setAxisAngle, getRotationAngle TransformComponent.position Descripción: Propiedad de tipo Vector3 que obtiene o modifica la posición en la
matriz de transformación.
Ver también: scale, eulerAngles TransformComponent:setAxisAngle(axis,angle) Descripción: Aplica a la matriz de transformación una rotación determinada por
un ángulo sobre el eje indicado
Parámetros: - axis: Vector3 que indica el eje sobre el que aplicar la rotación. - angle: Escalar con el ángulo de la rotación en radianes
Devuelve:
Ver también: eulerAngles, getRotationAxis, getRotationAngle TransformComponent.scale Descripción: Propiedad de tipo Vector3 que obtiene o modifica la escala en la
matriz de transformación.
Ver también: position, eulerAngles
Daniel Ponsoda Montiel
185