Estudiante:
Ismael José Aquino Andújar
Matrícula:
2-16-5981
Sección:
INF-910-001
Materia:
PROG. DE VIDEO JUEGOS
Profesor/a:
HUASCAR FRIAS VILORIO
Introducción
Actualmente, la industria del videojuego goza de una muy buena salud a
nivel mundial, rivalizando en presupuesto con las industrias cinematográfica y
musical. En este capítulo se discute, desde una perspectiva general, el desarrollo
de videojuegos, haciendo hincapié en su evolución y en los distintos elementos
involucrados en este complejo proceso de desarrollo. En la segunda parte del
capítulo se introduce el concepto de arquitectura del motor, como eje
fundamental para el diseño y desarrollo de videojuegos comerciales.
El objetivo de este módulo, titulado «Arquitectura del Motor» dentro del Curso
de Experto en Desarrollo de Videojuegos, es introducir los conceptos básicos
relativos a las estructuras y principios de diseño y desarrollo comúnmente
empleados en la creación de videojuegos.
Para ello, uno de los principales objetivos es proporcionar una visión general de
la arquitectura general de un motor de juegos. Dentro del contexto de esta
arquitectura general se hace hincapié en aspectos como los subsistemas de bajo
nivel, el bucle de juego, la gestión básica de recursos, como el sonido, y la
gestión de la concurrencia. Para llevar a cabo una discusión práctica de todos
estos elementos se hace uso del motor de renderizado Ogre3D.
Por otra parte, en este primer módulo también se estudian los fundamentos del
lenguaje de programación C++ como herramienta fundamental para el
desarrollo de videojuegos profesionales. Este estudio se complementa con una
discusión en profundidad de una gran variedad de patrones de diseño y de la
biblioteca STL. Además, también se realiza un recorrido por herramientas que
son esenciales en el desarrollo de proyectos software complejos, como por
ejemplo los sistemas de control de versiones, o procesos como la compilación o
la depuración.
En el capítulo 5 se estudia el uso de la biblioteca STL y se discute su uso en el
ámbito del desarrollo de videojuegos.
El hecho de que end () devuelva un iterador al siguiente elemento al último
albergado en el contenedor es una convención en STL que simplifica la
iteración sobre los elementos del mismo o la implementación de algoritmos.
Una vez que se obtiene un iterador que apunta a una parte del contenedor, es
posible utilizarlo como referencia para acceder a los elementos contiguos. En
función del tipo de iterador y de la funcionalidad que implemente, será posible
acceder solamente al elemento contiguo, al contiguo y al anterior o a uno
aleatorio.
El siguiente fragmento de código muestra un ejemplo sencillo en el que se
utiliza STL para instanciar un vector de enteros, añadir elementos y, finalmente,
recorrerlo haciendo uso de un iterador. Como se puede apreciar en la línea STL
hace uso de plantillas para manejar los distintos contenedores, permitiendo
separar la funcionalidad de los mismos respecto a los datos que contienen. En el
ejemplo, se instancia un vector de tipos entero.
STL y el desarrollo de videojuegos
En la sección 1.2 se discutió una visión general de la arquitectura estructurada
en capas de un motor de juegos. Una de esas capas es la que proporciona
bibliotecas de desarrollo, herramientas transversales y middlewares con el
objetivo de dar soporte al proceso de desarrollo de un videojuego (ver sección
1.2.2). STL estaría incluido en dicha capa como biblioteca estándar de C++,
posibilitando el uso de diversos contenedores como los mencionados
anteriormente.
Desde un punto de vista general, a la hora de abordar el desarrollo de un
videojuego, el programador o ingeniero tendría que decidir si utilizar STL o, por
el contrario, utilizar alguna otra biblioteca que se adapte mejor a los requisitos
impuestos por el juego a implementa.
Reutilización de código
Uno de los principales argumentos para utilizar STL en el desarrollo de
videojuegos es la reutilización de código que ya ha sido implementado,
depurado y portado a distintas plataformas. En este contexto, STL proporciona
directamente estructuras de datos y algoritmos que se pueden utilizar y aplicar,
respectivamente, para implementar soluciones dependientes de un dominio,
como por ejemplo los videojuegos.
Gran parte del código de STL está vinculado a la construcción de bloques
básicos en el desarrollo de videojuegos, como las listas, las tablas hash, y a
algoritmos fundamentales como la ordenación o la búsqueda de elementos.
Además, el diseño de STL está basado en el uso extensivo de plantillas. Por este
motivo, es posible utilizarlo para manejar cualquier tipo de estructura de datos
sin tener que modificar el diseño interno de la propia aplicación.
Rendimiento
Uno de los aspectos críticos en el ámbito del desarrollo de videojuegos es el
rendimiento, ya que es fundamental para lograr una sensación de interactividad
y para dotar de sensación de realismo el usuario de videojuegos. Recuerde que
un videojuego es una aplicación gráfica de renderizado en tiempo real y, por lo
tanto, es necesario asegurar una tasa de frames por segundo adecuada y en todo
momento.
Para ello, el rendimiento de la aplicación es crítico y éste viene determinado en
gran medida por las herramientas utilizadas. En general, STL proporciona un
muy buen rendimiento debido principalmente a que ha sido mejorado y
optimizado por cientos de desarrolladores en estos últimos años, considerando
las propiedades intrínsecas de cada uno de sus elementos y de las plataformas
sobre las que se ejecutará.
No obstante, algunas compañías tan relevantes en el ámbito del desarrollo de
videojuegos, como EA (Electronic Arts), han liberado su propia adaptación de
STL denominada EASTL (Electronic Arts Standard Template Library) 1,
justificando esta decisión en base a la detección de ciertas debilidades, como el
modelo de asignación de memoria, o el hecho de garantizar la consistencia
desde el punto de vista de la portabilidad.
Además del rendimiento de STL, es importante considerar que su uso permite
que el desarrollador se centre en el manejo de elementos propios, es decir, a
nivel de nodos en una lista o claves en un diccionario, en lugar de prestar más
importancia a elementos de más bajo nivel, como punteros o buffers de
memoria.
Vector
El vector es uno de los contenedores más simples y utilizados de STL, ya que
posibilita la inserción y eliminación de elementos en cualquier posición. Sin
embargo, la complejidad computacional de dichas operaciones depende de la
posición exacta en la que se inserta o elimina, respectivamente, el elemento en
cuestión. Dicha complejidad determina el rendimiento de la operación y, por lo
tanto, el rendimiento global del contenedor.
Un aspecto importante del vector es que proporciona iteradores bidireccionales,
es decir, es posible acceder tanto al elemento posterior como al elemento
anterior a partir de un determinado iterador. Así mismo, el vector permite el
acceso directo sobre un determinado elemento, de manera similar al acceso en
los arrays de C.
A diferencia de lo que ocurre con los arrays, un vector no tiene límite en cuanto
al número de elementos que se pueden añadir. Al menos, mientras el sistema
tenga memoria disponible. En caso de utilizar un array, el programador ha de
comprobar continuamente el tamaño de este para asegurarse de que la inserción
es factible, evitando así potenciales violaciones de segmento. En este contexto,
el vector representa una solución a este tipo de problemas.
El bucle de renderizado
Hace años, cuando aún el desarrollo de videojuegos 2D era el estándar en la
industria, uno de los principales objetivos de diseño de los juegos era minimizar
el número de píxeles a dibujar por el pipelinede renderizado con el objetivo de
maximizar la tasa de fps del juego. Evidentemente, si en cada una de las
iteraciones del bucle de renderizado el número de píxeles que cambia es
mínimo, el juego correrá a una mayor velocidad.
Esta técnica es en realidad muy parecida a la que se plantea en el desarrollo de
interfaces gráficas de usuario (GUI (Graphical User Interfaz)), donde gran parte
de estas es estática y sólo se producen cambios, generalmente, en algunas partes
bien definidas. Este planteamiento, similar al utilizado en el desarrollo de
videojuegos 2D antiguos, está basado en redibujar únicamente aquellas partes
de la pantalla cuyo contenido cambia.
En el desarrollo de videojuegos 3D, aunque manteniendo la idea de dibujar el
mínimo número de primitivas necesarias en cada iteración del bucle de
renderizado, la filosofía es radicalmente distinta. En general, al mismo tiempo
que la cámara se mueve en el espacio tridimensional, el contenido audiovisual
cambia continuamente, por lo que no es viable aplicar técnicas tan simples
como la mencionada anteriormente.
Arquitecturas típicas del bucle de juego
La arquitectura del bucle de juego se puede implementar de diferentes formas
mediante distintos planteamientos. Sin embargo, la mayoría de ellos tienen en
común el uso de uno o varios bucles de control que gobiernan la actualización e
interacción con los distintos componentes del motor de juegos. En esta sección
se realiza un breve recorrido por las alternativas más populares, resaltando
especialmente un planteamiento basado en la gestión de los distintos estados por
los que puede atravesar un juego. Esta última alternativa se discutirá con un
caso de estudio detallado que hace uso de Ogre.
Tratamiento de mensajes en Windows
En las plataformas WindowsTM, los juegos han de atender los mensajes
recibidos por el propio sistema operativo y dar soporte a los distintos
componentes del propio motor de juego. Típicamente, en estas plataformas se
implementan los denominados message pumps [5], como responsables del
tratamiento de este tipo de mensajes.
Desde un punto de vista general, el planteamiento de este esquema consiste en
atender los mensajes del propio sistema operativo cuando llegan, interactuando
con el motor de juegos cuando no existan mensajes del sistema operativo por
procesar. En ese caso se ejecuta una iteración del bucle de juego y se repite el
mismo proceso.
La principal consecuencia de este enfoque es que los mensajes del sistema
operativo tienen prioridad con respecto a aspectos críticos como el bucle de
renderizado. Por ejemplo, si la propia ventana en la que se está ejecutando el
juego se arrastra o su tamaño cambia, entonces el juego se congelará a la espera
de finalizar el tratamiento de eventos recibidos por el propio sistema operativo.
Tratamiento de eventos
En el ámbito de los juegos, un evento representa un cambio en el estado del
propio juego o en el entorno. Un ejemplo muy común está representado por el
jugador cuando pulsa un botón del joystick, pero también se pueden identificar
eventos a nivel interno, como por ejemplo la reaparición o respawn de un NPC
en el juego.
Gran parte de los motores de juegos incluyen un subsistema específico para el
tratamiento de eventos, permitiendo al resto de componentes del motor o
incluso a entidades específicas registrarse como partes interesadas en un
determinado tipo de eventos. Este planteamiento está muy estrechamente
relacionado con el patrón Observer.
Esquema basado en estados
Desde un punto de vista general, los juegos se pueden dividir en una serie de
etapas o estados que se caracterizan no sólo por su funcionamiento sino también
por la interacción con el usuario o jugador. Típicamente, en la mayor parte de
los juegos es posible diferenciar los siguientes estados:
• Introducción o presentación, en la que se muestra al usuario aspectos
generales del juego, como por ejemplo la temática de este o incluso cómo
jugar.
• Menú principal, en la que el usuario ya puede elegir entre los distintos
modos de juegos y que, normalmente, consiste en una serie de entradas
textuales identificando las opciones posibles.
• Juego, donde ya es posible interactuar con la propia aplicación e ir
completando los objetivos marcados.
• Finalización o game over, donde se puede mostrar información sobre la
partida previamente jugada.
Evidentemente, esta clasificación es muy general ya que está planteada desde un
punto de vista muy abstracto. Por ejemplo, si consideramos aspectos más
específicos como por ejemplo el uso de dispositivos como PlayStation
MoveTM, WiimoteTMo KinectTM, sería necesario incluir un estado de
calibración antes de poder utilizar estos dispositivos de manera satisfactoria.
Gestión básica de recursos
En esta sección se discute la gestión básica de los recursos, haciendo hincapié
en dos casos de estudio concretos: i) la gestión básica del sonido y ii) el sistema
de archivos. Antes de llevar a cabo un estudio específico de estas dos
cuestiones, la primera sección de este capítulo introduce la problemática de la
gestión de recursos en los motores de juegos. Así mismo, se discuten las
posibilidades que el framework Ogre3D ofrece para gestionar dicha
problemática.
En el caso de la gestión básica del sonido, el lector será capaz de manejar una
serie de abstracciones, en torno a la biblioteca multimedia SDL, para integrar
música y efectos de sonido en los juegos que desarrolle. Por otra parte, en la
sección relativa a la gestión del sistema de archivos se planteará la problemática
del tratamiento de archivos y se estudiarán técnicas de entrada/salida asíncrona.
Debido a la naturaleza multimedia de los motores de juegos, una consecuencia
directa es la necesidad de gestionar distintos tipos de datos, como por ejemplo
geometría tridimensional, texturas, animaciones, sonidos, datos relativos a la
gestión de física y colisiones, etc. Evidentemente, esta naturaleza tan variada se
ha de gestionar de forma consistente y garantizando la integridad de los datos.
Gestión básica del sonido
Introducción a SDL SDL (Simple Directmedia Layer) es una biblioteca
multimedia y multiplataforma ampliamente utilizada en el ámbito del desarrollo
de aplicaciones multimedia. Desde un punto de vista funcional, SDL
proporciona una serie de APIs para el manejo de vídeo, audio, eventos de
entrada, multi-hilo o renderizado con OpenGL, entre otros aspectos. Desde un
punto de vista abstracto, SDL proporciona una API consistente de manera
independiente a la plataforma de desarrollo.
SDL está bien estructurado y es fácil de utilizar. La filosofía de su diseño se
puede resumir en ofrecer al desarrollador diversas herramientas que se pueden
utilizar de manera independiente, en lugar de manejar una biblioteca software
de mayor envergadura. Por ejemplo, un juego podría hacer uso de la biblioteca
SDL únicamente para la gestión de sonido, mientras que la parte gráfica sería
gestionada de manera independiente (utilizando Ogre3D, por ejemplo).
SDL se puede integrar perfectamente con OpenGL para llevar a cabo la
inicialización de la parte gráfica de una aplicación interactiva, delegando en
SDL el propio tratamiento de los eventos de entrada.
Aspectos fundamentales
El subsistema de arranque y parada es el responsable de llevar a cabo la
inicialización y configuración de los distintos subsistemas que forman parte del
motor de juegos, así como de realizar la parada de estos cuando así sea
necesario. Este sistema forma parte de la capa de subsistemas principales que se
introdujo brevemente en la sección 1.2.4. La figura 7.2 muestra la interacción
del subsistema de arranque y parada con el resto de los componentes de la
arquitectura general del motor de juegos.
Este subsistema juega un papel básico pero fundamental dentro de la
arquitectura del motor de juegos, ya que disponer de una entidad software que
conozca las interdependencias entre el resto de los subsistemas es crucial para
efectuar su arranque, configuración y parada de una manera adecuada. Desde un
punto de vista general, si un subsistema S tiene una dependencia con respecto a
un subsistema T, entonces el subsistema de arranque ha de tener en cuenta dicha
dependencia para arrancar primero T y, a continuación, S. Así mismo, la parada
de dichos subsistemas se suele realizar generalmente en orden inverso, es decir,
primero se pararía S y, posteriormente, T (ver figura 7.3).
Esquema típico de arranque y parada
En esta subsección se estudia un enfoque simple [5], aunque ampliamente
utilizado, para gestionar tanto el arranque como la parada de los distintos
subsistemas que forman la arquitectura de un motor de juegos. La idea principal
de este enfoque reside en explicitar el arranque y la parada de dichos
subsistemas, que a su vez se gestionan mediante managers implementados de
acuerdo con el patrón singleton.
Para ello, tanto el arranque como la parada de un subsistema se implementa
mediante funciones explícitas de arranque y parada, típicamente denominadas
startUp y shutDown, respectivamente. En esencia, estas funciones representan
al constructor y al destructor de la clase. Sin embargo, la posibilidad de realizar
llamadas permite controlar de manera adecuada la inicialización y parada de un
subsistema.
El siguiente listado de código muestra la implementación típica de un
subsistema de gestión haciendo uso del enfoque planteado en esta subsección.
Observe cómo tanto el constructor y el destructor de clase están vacíos, ya que
se delega la inicialización del subsistema en las funciones startUp() y
shutDown().
Caso de estudio. Ogre 3D
Aunque Ogre 3D es en realidad un motor de renderizado en lugar de un
completo motor de juegos, dicho entorno proporciona una gran cantidad de
subsistemas relevantes para facilitar el desarrollo de videojuegos. Entre ellos,
también existe un subsistema de arranque y parada que hace gala de una gran
simplicidad y sencillez.
Básicamente, el arranque y la parada en Ogre se realiza a través de la clase
Ogre:Root, la cual implementa el patrón singleton con el objetivo de asegurar
una única instancia de dicha clase, la cual actúa como punto central de gestión.
Para ello, la clase Root almacena punteros, como variables miembro privadas, a
todos los subsistemas soportados por Ogre con el objetivo de gestionar su
creación y su destrucción. El siguiente listado de código muestra algunos
aspectos relevantes de la declaración de esta clase.
El objeto Root representa el punto de entrada de Ogre y ha de ser el primer
objeto instanciado en una aplicación y el último en ser destruido. Típicamente,
la clase principal de los ejemplos básicos desarrollados tendrán como variable
miembro una instancia de la clase Root, con el objetivo de facilitar la
administración del juego en cuestión.
Desde el punto de vista del renderizado, el objeto de tipo Root proporciona la
función startRendering. Cuando se realiza una llamada a dicha función, la
aplicación entrará en un bucle de renderizado continuo que finalizará cuando
todas las ventanas gráficas se hayan cerrado o cuando todos los objetos del tipo
FrameListener finalicen su ejecución (ver módulo 2, Programación Gráfica).
La implementación del constructor de la clase Ogre:Root tiene como objetivo
principal instanciar y arrancar los distintos subsistemas previamente declarados
en el anterior listado de código. A continuación, se muestran algunos aspectos
relevantes de la implementación de dicho constructor.
Caso de estudio
Quake III El código fuente de Quake III1 fue liberado bajo licencia GPLv2 el
día 20 de agosto de 2005. Desde entonces, la comunidad aficionada de
desarrollo ha realizado modificaciones y mejoras e incluso el propio motor se ha
reutilizado para el desarrollo de otros juegos. El diseño de los motores de Quake
es un muy buen ejemplo de arquitectura bien estructurada y modularizada,
hecho que posibilita el estudio de su código y la adquisición de experiencia por
el desarrollador de videojuegos.
En esta sección se estudiará desde un punto de vista general el sistema de
arranque y de parada de Quake III. Al igual que ocurre en el caso de Ogre, el
esquema planteado consiste en hacer uso de funciones específicas para el
arranque, inicialización y parada de las distintas entidades o subsistemas
involucrados.
Contenedores
Como ya se introdujo en el capítulo 5, los contenedores son simplemente
objetos que contienen otros objetos. En el mundo del desarrollo de videojuegos,
y en el de las aplicaciones software en general, los contenedores se utilizan
extensivamente para almacenar las estructuras de datos que conforman la base
del diseño que soluciona un determinado problema.
Algunos de los contenedores más conocidos ya se comentaron en las secciones
5.3, 5.4 y 5.5. En dichas secciones se hizo un especial hincapié en relación con
la utilización de estos contenedores, atendiendo a sus principales características,
como la definición subyacente en la biblioteca STL.
Más allá de STL
Como ya se discutió anteriormente en el capítulo 5, gran parte de los motores de
juego tienen sus propias implementaciones de los contenedores más utilizados
para el manejo de sus estructuras de datos. Este planteamiento está muy
extendido en el ámbito de las consolas de sobremesa, las consolas portátiles, los
teléfonos móviles y las PDA (Personal Digital Assistant) s. Los principales
motivos son los siguientes:
• Control total sobre los contenedores y estructuras de datos desarrolladas,
especialmente sobre el mecanismo de asignación de memoria, aunque sin
olvidar aspectos como los propios algoritmos. Aunque STL permite crear
asignadores de memoria personalizados (custom allocators), en ocasiones
los propios patrones de los contenedores de STL pueden ser insuficientes.
• Optimizaciones, considerando el propio hardware sobre el que se
ejecutará el motor. STL es un estándar independiente de la plataforma y
el sistema operativo, por lo que no es posible aplicar optimizaciones de
manera directa sobre la propia biblioteca.
•
• Personalización debido a la necesidad de incluir funcionalidad sobre un
contenedor que no esté inicialmente considerada en otras bibliotecas
como STL. Por ejemplo, en un determinado problema puede ser necesario
obtener los n elementos más adecuados para satisfacer una necesidad.
Esquemas típicos de configuración
Las variables de configuración se pueden definir de manera trivial
mediante el uso de variables globales o variables miembro de una clase
que implemente el patrón singleton. Sin embargo, idealmente debería ser
posible modificar dichas variables de configuración sin necesidad de
modificar el código fuente y, por lo tanto, volver a compilar para generar
un ejecutable.
Normalmente, las variables o parámetros de configuración residen en
algún tipo de dispositivo de almacenamiento externo, como por ejemplo
un disco duro o una tarjeta de memoria. De este modo, es posible
almacenar y recuperar la información asociada a la configuración de una
manera práctica y directa.
A continuación, se discute brevemente las distintas aproximaciones más
utilizadas en el desarrollo de videojuegos. En primer lugar, uno de los
esquemas típicos de recuperación y almacenamiento de información está
representado por los ficheros de configuración. La principal ventaja de
este enfoque es que son perfectamente legibles ya que suelen especificar
de manera explícita la variable de configuración a modificar y el valor
asociada a la misma. En general, cada motor de juegos mantiene su
propio convenio, aunque, normalmente, todos se basan en una secuencia
de pares clave-valor.