M1 Arquitectura Del Motor
M1 Arquitectura Del Motor
M1 Arquitectura Del Motor
David Vallejo
Cleto Martn
Ttulo:
Desarrollo de Videojuegos: Un Enfoque Prctico
Subttulo: Volumen 1. Arquitectura del Motor
Edicin: Septiembre 2015
Autores: David Vallejo Fernndez, Cleto Martn
Angelina
ISBN:
978-1517309558
Edita:
David Vallejo, Carlos Gonzlez y David Villa
Portada: (Ilustracin) Vctor Barba Pizarro
Diseo: Carlos Gonzlez Morcillo y Vctor Barba Pizarro
Printed by CreateSpace, an Amazon.com company
Available from Amazon.com and other online stores
Este libro fue compuesto con LaTeX a partir de una plantilla de David Villa
Alises y Carlos Gonzlez Morcillo. Maquetacin final de Carlos Guijarro
Fernndez-Arroyo y David Vallejo Fernndez.
Creative Commons License: Usted es libre de copiar, distribuir y comunicar pblicamente la obra, bajo
las condiciones siguientes: 1. Reconocimiento. Debe reconocer los crditos de la obra de la manera
especicada por el autor o el licenciador. 2. No comercial. No puede utilizar esta obra para nes
comerciales. 3. Sin obras derivadas. No se puede alterar, transformar o generar una obra derivada a
partir de esta obra. Ms informacin en: http://creativecommons.org/licenses/by-nc-nd/3.0/
Prefacio
Requisitos previos
Este libro tiene un pblico objetivo con un perfil principalmente tcnico. Al igual
que el curso, est orientado a la capacitacin de profesionales de la programacin de
videojuegos. De esta forma, este libro no est orientado para un pblico de perfil
artstico (modeladores, animadores, msicos, etc.) en el mbito de los videojuegos.
Se asume que el lector es capaz de desarrollar programas de nivel medio en C++.
Aunque se describen algunos aspectos clave de C++ a modo de resumen, es
recomendable refrescar los conceptos bsicos con alguno de los libros recogidos en
la bibliografa del curso. De igual modo, se asume que el lector tiene conocimientos
de estructuras de datos y algoritmia. El libro est orientado principalmente para
titulados o estudiantes de ltimos cursos de Ingeniera en Informtica.
Agradecimientos
Los autores del libro quieren agradecer en primer lugar a los alumnos de las cuatro
ediciones del Curso de Experto en Desarrollo de Videojuegos por su participacin en
el mismo y el excelente ambiente en las clases, las cuestiones planteadas y la pasin
demostrada en el desarrollo de todos los trabajos.
Los autores tambin agradecen el soporte del personal de administracin y servicios
de la Escuela Superior de Informtica de Ciudad Real, a la propia Escuela y el
Departamento de Tecnologas y Sistema de Informacin de la Universidad de
Castilla-La Mancha.
De igual modo, se quiere reflejar especialmente el agradecimiento a las empresas
que ofertarn prcticas en la 3a edicin del curso: Devilish Games (Alicante),
Dolores Entertainment (Barcelona), from the bench (Alicante), Iberlynx Mobile
Solutions (Ciudad Real), Kitmaker (Palma), playspace (Palma), totemcat - Materia
Works (Madrid) y Zuinqstudio (Sevilla). Este agradecimiento se extiende a los
portales y blogs del mundo de los videojuegos que han facilitado la difusin de este
material, destacando a Meristation, Eurogamer, Genbeta Dev, Vidaextra y
HardGame2.
Finalmente, los autores desean agradecer su participacin a las entidades
colaboradoras del curso: Indra Software Labs, la asociacin de desarrolladores de
videojuegos Stratos y Libro Virtual.
Autores de la Coleccin
[IV]
2.2.2. Compilando con GCC . . . . . . . . . .
2.2.3. Cmo funciona GCC? . . . . . . . . . .
2.2.4. Ejemplos . . . . . . . . . . . . . . . . .
2.2.5. Otras herramientas . . . . . . . . . . . .
2.2.6. Depurando con GDB . . . . . . . . . . .
2.2.7. Construccin automtica con GNU Make
2.3. Gestin de proyectos y documentacin . . . . . .
2.3.1. Sistemas de control de versiones . . . . .
2.3.2. Sistemas de integracin continua . . . . .
2.3.3. Documentacin . . . . . . . . . . . . . .
2.3.4. Forjas de desarrollo . . . . . . . . . . . .
NDICE GENERAL
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
31
32
34
39
40
46
51
51
60
62
65
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
67
67
67
68
69
70
73
77
77
79
83
86
87
89
92
97
97
98
101
102
102
104
107
109
4. Patrones de Diseo
111
4.1. Introduccin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.1.1. Elementos bsicos de diseo . . . . . . . . . . . . . . . . . . . . 112
[V]
4.2.
4.3.
4.4.
4.5.
4.6.
5. La Biblioteca STL
5.1. Visin general de STL . . . . . .
5.2. STL y el desarrollo de videojuegos
5.2.1. Reutilizacin de cdigo . .
5.2.2. Rendimiento . . . . . . .
5.2.3. Inconvenientes . . . . . .
5.3. Secuencias . . . . . . . . . . . . .
5.3.1. Vector . . . . . . . . . . .
5.3.2. Deque . . . . . . . . . . .
5.3.3. List . . . . . . . . . . . .
5.4. Contenedores asociativos . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
113
114
115
118
118
120
122
124
125
126
127
129
131
133
134
136
136
138
139
142
143
144
146
149
150
151
151
152
.
.
.
.
.
.
.
.
.
.
155
155
158
159
159
160
161
161
164
166
169
[VI]
5.4.1. Set y multiset . .
5.4.2. Map y multimap
5.5. Adaptadores de secuencia
5.5.1. Stack . . . . . .
5.5.2. Queue . . . . . .
5.5.3. Cola de prioridad
NDICE GENERAL
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
170
172
175
176
176
177
6. Gestin de Recursos
6.1. El bucle de juego . . . . . . . . . . . . . . . . . . . .
6.1.1. El bucle de renderizado . . . . . . . . . . . . .
6.1.2. Visin general del bucle de juego . . . . . . .
6.1.3. Arquitecturas tpicas del bucle de juego . . . .
6.1.4. Gestin de estados de juego con Ogre3D . . .
6.1.5. Definicin de estados concretos . . . . . . . .
6.2. Gestin bsica de recursos . . . . . . . . . . . . . . .
6.2.1. Gestin de recursos con Ogre3D . . . . . . . .
6.2.2. Gestin bsica del sonido . . . . . . . . . . . .
6.3. El sistema de archivos . . . . . . . . . . . . . . . . . .
6.3.1. Gestin y tratamiento de archivos . . . . . . .
6.3.2. E/S bsica . . . . . . . . . . . . . . . . . . . .
6.3.3. E/S asncrona . . . . . . . . . . . . . . . . . .
6.3.4. Caso de estudio. La biblioteca Boost.Asio C++
6.3.5. Consideraciones finales . . . . . . . . . . . . .
6.4. Importador de datos de intercambio . . . . . . . . . .
6.4.1. Formatos de intercambio . . . . . . . . . . . .
6.4.2. Creacin de un importador . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
179
179
179
180
182
185
191
193
194
195
206
206
209
212
213
219
219
219
222
.
.
.
.
.
.
.
.
.
.
.
.
229
230
230
234
235
238
241
241
243
247
247
248
250
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
[VII]
7.4. Configuracin del motor . . . . . . . . . . . . . . . . . . . . .
7.4.1. Esquemas tpicos de configuracin . . . . . . . . . . . .
7.4.2. Caso de estudio. Esquemas de definicin. . . . . . . . .
7.5. Fundamentos bsicos de concurrencia . . . . . . . . . . . . . .
7.5.1. El concepto de hilo . . . . . . . . . . . . . . . . . . . .
7.5.2. El problema de la seccin crtica . . . . . . . . . . . . .
7.6. La biblioteca de hilos de I CE . . . . . . . . . . . . . . . . . . .
7.6.1. Internet Communication Engine . . . . . . . . . . . . .
7.6.2. Manejo de hilos . . . . . . . . . . . . . . . . . . . . . .
7.6.3. Exclusin mutua bsica . . . . . . . . . . . . . . . . . .
7.6.4. Flexibilizando el concepto de mutex . . . . . . . . . . .
7.6.5. Introduciendo monitores . . . . . . . . . . . . . . . . .
7.7. Concurrencia en C++11 . . . . . . . . . . . . . . . . . . . . . .
7.7.1. Filsofos comensales en C++11 . . . . . . . . . . . . .
7.8. Multi-threading en Ogre3D . . . . . . . . . . . . . . . . . . . .
7.9. Caso de estudio. Procesamiento en segundo plano mediante hilos
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
251
251
253
254
254
255
256
256
257
260
263
264
270
271
273
277
[X]
IGC
I CE
KISS
LIFO
MMOG
MVC
NPC
ODE
ODT
OGRE
PDA
PDF
PHP
POO
POSIX
PPC
RPG
RST
RTS
SAX
SDK
SDL
SGBD
STL
TCP
VCS
XML
YAML
ACRNIMOS
In-Game Cinematics
Internet Communication Engine
Keep it simple, Stupid!
Last In, First Out
Massively Multiplayer Online Game
Model View Controller
Non-Player Character
Open Dynamics Engine
OpenDocument Text
Object-Oriented Graphics Rendering Engine
Personal Digital Assistant
Portable Document Format
Personal Home Page
Programacin Orientada a Objetos
Portable Operating System Interface X
PowerPC
Role-Playing Games
reStructuredText
Real-Time Strategy
Simple API for XML
Software Development Kit
Simple Directmedia Layer
Sistema de Gestin de Base de Datos
Standard Template Library
Transport Control Protocol
Version Control System
eXtensible Markup Language
YAML Aint Markup Language
[2]
Captulo 1 :: Introduccin
[3]
1 http://www.idsoftware.com/games/quake/quake/
2 http://www.unrealengine.com/
3 http://www.havok.com
[4]
Captulo 1 :: Introduccin
Productor ejecutivo
Productor
Equipo de marketing
Director artstico
Director tcnico
Diseador jefe
Gestor de pruebas
Artista jefe
Programador jefe
Equipo de diseo
Equipo de pruebas
Equipo artstico
Conceptual
Modelado
Animacin
Texturas
Artista
tcnico
Programador
Networking
Herramientas
Fsica
Inteligencia Art.
Motor
Interfaces
Audio
Figura 1.2: Visin conceptual de un equipo de desarrollo de videojuegos, considerando especialmente la parte
de programacin.
Artistas de concepto, responsables de crear bocetos que permitan al resto del equipo
hacerse una idea inicial del aspecto final del videojuego. Su trabajo resulta especialmente importante en las primeras fases de un proyecto.
Modeladores, responsables de generar el contenido 3D del videojuego, como por
ejemplo los escenarios o los propios personajes que forman parte del mismo.
[5]
Artistas de texturizado, responsables de crear las texturas o imgenes bidimensionales que formarn parte del contenido visual del juego. Las texturas se aplican
sobre la geometra de los modelos con el objetivo de dotarlos de mayor realismo.
Artistas de iluminacin, responsables de gestionar las fuentes de luz del videojuego,
as como sus principales propiedades, tanto estticas como dinmicas.
Animadores, responsables de dotar de movimientos a los personajes y objetos dinmicos del videojuego. Un ejemplo tpico de animacin podra ser el movimiento
de brazos de un determinado carcter.
Actores de captura de movimiento, responsables de obtener datos de movimiento
reales para que los animadores puedan integrarlos a la hora de animar los personajes.
Diseadores de sonido, responsables de integrar los efectos de sonido del videojuego.
Otros actores, responsables de diversas tareas como por ejemplo los encargados de
dotar de voz a los personajes.
Al igual que suele ocurrir con los ingenieros, existe el rol de artista senior cuyas
responsabilidades tambin incluyen la supervisin de los numerosos aspectos vinculados
al componente artstico.
Los diseadores de juego son los responsaScripting e IA
bles de disear el contenido del juego, destacanEl uso de lenguajes de alto nivel es basdo la evolucin del mismo desde el principio hastante comn en el desarrollo de videota el final, la secuencia de captulos, las reglas del
juegos y permite diferenciar claramente la lgica de la aplicacin y la propia
juego, los objetivos principales y secundarios, etc.
implementacin. Una parte significatiEvidentemente, todos los aspectos de diseo estn
va de las desarrolladoras utiliza su proestrechamente ligados al propio gnero del mismo.
pio lenguaje de scripting, aunque exisPor ejemplo, en un juego de conduccin es tarea
ten lenguajes ampliamente utilizados,
como son Lua o Python.
de los diseadores definir el comportamiento de los
coches adversarios ante, por ejemplo, el adelantamiento de un rival.
Los diseadores suelen trabajar directamente con los ingenieros para afrontar diversos
retos, como por ejemplo el comportamiento de los enemigos en una aventura. De hecho, es
bastante comn que los propios diseadores programen, junto con los ingenieros, dichos
aspectos haciendo uso de lenguajes de scripting de alto nivel, como por ejemplo Lua4 o
Python5 .
Como ocurre con las otras disciplinas previamente comentadas, en algunos estudios
los diseadores de juego tambin juegan roles de gestin y supervisin tcnica.
Finalmente, en el desarrollo de videojuegos tambin estn presentes roles vinculados a
la produccin, especialmente en estudios de mayor capacidad, asociados a la planificacin
del proyecto y a la gestin de recursos humanos. En algunas ocasiones, los productores
tambin asumen roles relacionados con el diseo del juego. As mismo, los responsables
de marketing, de administracin y de soporte juegan un papel relevante. Tambin resulta importante resaltar la figura de publicador como entidad responsable del marketing
y distribucin del videojuego desarrollado por un determinado estudio. Mientras algunos estudios tienen contratos permanentes con un determinado publicador, otros prefieren
mantener una relacin temporal y asociarse con el publicador que le ofrezca mejores condiciones para gestionar el lanzamiento de un ttulo.
4 http://www.lua.org
5 http://www.python.org
[6]
Captulo 1 :: Introduccin
El usuario visualiza una imagen renderizada por la aplicacin en la pantalla o dispositivo de visualizacin.
El usuario acta en funcin de lo que haya visualizado, interactuando directamente
con la aplicacin, por ejemplo mediante un teclado.
En funcin de la accin realizada por el usuario, la aplicacin grfica genera una
salida u otra, es decir, existe una retroalimentacin que afecta a la propia aplicacin.
En el caso de los videojuegos, este ciclo de visualizacin, actuacin y renderizado
ha de ejecutarse con una frecuencia lo suficientemente elevada como para que el usuario
se sienta inmerso en el videojuego, y no lo perciba simplemente como una sucesin de
imgenes estticas. En este contexto, el frame rate se define como el nmero de imgenes
por segundo, comnmente fps, que la aplicacin grfica es capaz de generar. A mayor
frame rate, mayor sensacin de realismo en el videojuego. Actualmente, una tasa de 30
fps se considera ms que aceptable para la mayora de juegos. No obstante, algunos juegos
ofrecen tasas que doblan dicha medida.
Generalmente, el desarrollador de videojuegos ha de buscar un compromiso entre los
fps y el grado de realismo del videojuego. Por ejemplo, el uso de modelos con una
alta complejidad computacional, es decir, con un mayor nmero de polgonos, o la
integracin de comportamientos inteligentes por parte de los enemigos en un juego,
o NPC (Non-Player Character), disminuir los fps.
En otras palabras, los juegos son aplicaciones interactivas que estn marcadas por el
tiempo, es decir, cada uno de los ciclos de ejecucin tiene un deadline que ha de cumplirse
para no perder realismo.
[7]
Aunque el componente grfico representa gran parte de la complejidad computacional de los videojuegos, no es el nico. En cada ciclo de ejecucin, el videojuego ha de
tener en cuenta la evolucin del mundo en el que se desarrolla el mismo. Dicha evolucin depender del estado de dicho mundo en un momento determinado y de cmo las
distintas entidades dinmicas interactan con l. Obviamente, recrear el mundo real con
un nivel de exactitud elevado no resulta manejable ni prctico, por lo que normalmente
dicho mundo se aproxima y se simplifica, utilizando modelos matemticos para tratar con
su complejidad. En este contexto, destaca por ejemplo la simulacin fsica de los propios
elementos que forman parte del mundo.
Por otra parte, un juego tambin est ligado al comportamiento del personaje principal y del resto de entidades que existen dentro del mundo virtual. En el mbito
acadmico, estas entidades se suelen definir como agentes (agents) y se encuadran dentro de la denominada simulacin basada en agentes [10]. Bsicamente, este tipo
de aproximaciones tiene como objetivo dotar a los NPC
con cierta inteligencia para incrementar el grado de realismo de un juego estableciendo, incluso, mecanismos de
cooperacin y coordinacin entre los mismos. Respecto
al personaje principal, un videojuego ha de contemplar
las distintas acciones realizadas por el mismo, conside1.3: El motor de juego reprerando la posibilidad de decisiones impredecibles a priori Figura
senta el ncleo de un videojuego y
y las consecuencias que podran desencadenar.
determina el comportamiento de los
En resumen, y desde un punto de vista general, el distintos mdulos que lo componen.
desarrollo de un juego implica considerar un gran nmero de factores que, inevitablemente, incrementan la complejidad del mismo y, al mismo
tiempo, garantizar una tasa de fps adecuada para que la inmersin del usuario no se vea
afectada.
[8]
Captulo 1 :: Introduccin
[9]
Los FPS representan juegos con un desarrollo complejo, ya que uno de los retos principales que han de afrontar es la inmersin del usuario en un mundo hiperrealista que
ofrezca un alto nivel de detalle, al mismo tiempo que se garantice una alta reaccin de
respuesta a las acciones del usuario. Este gnero de juegos se centra en la aplicacin de
las siguientes tecnologas [5]:
Renderizado eficiente de grandes escenarios virtuales 3D.
Mecanismo de respuesta eficiente para controlar y apuntar con el personaje.
[10]
Captulo 1 :: Introduccin
Detalle de animacin elevado en relacin a las armas y los brazos del personaje
virtual.
Uso de una gran variedad de arsenal.
Sensacin de que el personaje flota sobre el escenario, debido al movimiento del
mismo y al modelo de colisiones.
NPC con un nivel de IA considerable y dotados de buenas animaciones.
7 http://www.idsoftware.com/business/techdownloads
8 http://mycryengine.com/
[11]
Dentro de este gnero resulta importante destacar los juegos de plataformas, en los que
el personaje principal ha de ir avanzado de un lugar a otro del escenario hasta alcanzar un
objetivo. Ejemplos representativos son las sagas de Super Mario, Sonic o Donkey Kong.
En el caso particular de los juegos de plataformas, el avatar del personaje tiene normalmente un efecto de dibujo animado, es decir, no suele necesitar un renderizado altamente
realista y, por lo tanto, complejo. En cualquier caso, la parte dedicada a la animacin del
personaje ha de estar especialmente cuidada para incrementar la sensacin de realismo a
la hora de controlarlo.
En los juegos en tercera persona, los desarrolladores han de prestar especial atencin
a la aplicacin de las siguientes tecnologas [5]:
Uso de plataformas mviles, equipos de escalado, cuerdas y otros modos de movimiento avanzados.
Inclusin de puzzles en el desarrollo del juego.
Uso de cmaras de seguimiento en tercera persona centradas en el personaje y que
posibiliten que el propio usuario las maneje a su antojo para facilitar el control del
personaje virtual.
Uso de un complejo sistema de colisiones asociado a la cmara para garantizar que
la visin no se vea dificultada por la geometra del entorno o los distintos objetos
dinmicos que se mueven por el mismo.
[12]
Captulo 1 :: Introduccin
[13]
Los juegos de conduccin se caracterizan por la necesidad de dedicar un esfuerzo considerable en alcanzar
una calidad grfica elevada en aquellos elementos cercanos a la cmara, especialmente el propio vehculo. Adems, este tipo de juegos, aunque suelen ser muy lineales,
mantienen una velocidad de desplazamiento muy elevada, directamente ligada a la del propio vehculo.
Al igual que ocurre en el resto de gneros previamente comentados, existen diversas tcnicas que pueden conFigura 1.7: Captura de pantalla del tribuir a mejorar la eficiencia de este tipo de juegos. Por
juego de conduccin Tux Racing, li- ejemplo, suele ser bastante comn utilizar estructuras de
cenciado bajo GPL por Jasmin Patry.
datos auxiliares para dividir el escenario en distintos tramos, con el objetivo de optimizar el proceso de renderizado o incluso facilitar el clculo de rutas ptimas utilizando tcnicas de IA [12]. Tambin
se suelen usar imgenes para renderizar elementos lejanos, como por ejemplo rboles,
vallas publicitarias u otro tipo de elementos.
Del mismo modo, y al igual que ocurre con los juegos en tercera persona, la cmara
tiene un papel relevante en el seguimiento del juego. En este contexto, el usuario normalmente tiene la posibilidad de elegir el tipo de cmara ms adecuado, como por ejemplo una cmara en primera persona, una en la que se visualicen los controles del propio
vehculo o una en tercera persona.
Otro gnero tradicional son los juegos de estrategia, normalmente clasificados en
tiempo real o RTS (Real-Time Strategy) y por turnos (turn-based strategy). Ejemplos
representativos de este gnero son Warcraft, Command & Conquer, Comandos, Age of
Empires o Starcraft, entre otros. Este tipo de juegos se caracterizan por mantener una
cmara con una perspectiva isomtrica, normalmente fija, de manera que el jugador tiene
una visin ms o menos completa del escenario, ya sea 2D o 3D. As mismo, es bastante
comn encontrar un gran nmero de unidades virtuales desplegadas en el mapa, siendo
responsabilidad del jugador su control, desplazamiento y accin.
Teniendo en cuenta las caractersticas generales de
este gnero, es posible plantear diversas optimizaciones.
Por ejemplo, una de las aproximaciones ms comunes en
este tipo de juegos consiste en dividir el escenario en una
rejilla o grid, con el objetivo de facilitar no slo el emplazamiento de unidades o edificios, sino tambin la planificacin de movimiento de un lugar del mapa a otro.
Por otra parte, las unidades se suelen renderizar con una
resolucin baja, es decir, con un bajo nmero de polgonos, con el objetivo de posibilitar el despliegue de un gran Figura 1.8: Captura de pantalla del
nmero de unidades de manera simultnea.
juego de estrategia en tiempo real 0
A.D., licenciado bajo GPL por WildFinalmente, en los ltimos aos ha aparecido un gne- firegames.
ro de juegos cuya principal caracterstica es la posibilidad
de jugar con un gran nmero de jugadores reales al mismo tiempo, del orden de cientos o
incluso miles de jugadores. Los juegos que se encuadran bajo este gnero se denominan
comnmente MMOG (Massively Multiplayer Online Game). El ejemplo ms representativo de este gnero es el juego World of Warcarft. Debido a la necesidad de soportar
un gran nmero de jugadores en lnea, los desarrolladores de este tipo de juegos han de
realizar un gran esfuerzo en la parte relativa al networking, ya que han de proporcionar un
servicio de calidad sobre el que construir su modelo de negocio, el cual suele estar basado
en suscripciones mensuales o anuales por parte de los usuarios.
[14]
Captulo 1 :: Introduccin
Al igual que ocurre en los juegos de estrategia, los MMOG suelen utilizar personajes
virtuales en baja resolucin para permitir la aparicin de un gran nmero de ellos en
pantalla de manera simultnea.
Adems de los distintos gneros mencionados en esta seccin, existen algunos ms
como por ejemplo los juegos deportivos, los juegos de rol o RPG (Role-Playing Games)
o los juegos de puzzles.
Antes de pasar a la siguiente seccin en la que se discutir la arquitectura general
de un motor de juego, resulta interesante destacar la existencia de algunas herramientas
libres que se pueden utilizar para la construccin de un motor de juegos. Una de las ms
populares, y que se utilizar en el presente curso, es OGRE 3D9 . Bsicamente, OGRE es
un motor de renderizado 3D bien estructurado y con una curva de aprendizaje adecuada.
Aunque OGRE no se puede definir como un motor de juegos completo, s que proporciona
un gran nmero de mdulos que permiten integrar funcionalidades no triviales, como
iluminacin avanzada o sistemas de animacin de caracteres.
1.2.
En esta seccin se plantea una visin general de la arquitectura de un motor de juegos [5], de manera independiente al gnero de los mismos, prestando especial importancia
a los mdulos ms relevantes desde el punto de vista del desarrollo de videojuegos.
Como ocurre con la gran mayora de sistemas software que tienen una complejidad
elevada, los motores de juegos se basan en una arquitectura estructurada en capas. De
este modo, las capas de nivel superior dependen de las capas de nivel inferior, pero no de
manera inversa. Este planteamiento permite ir aadiendo capas de manera progresiva y, lo
que es ms importante, permite modificar determinados aspectos de una capa en concreto
sin que el resto de capas inferiores se vean afectadas por dicho cambio.
A continuacin, se describen los principales mdulos que forman parte de la arquitectura que se expone en la figura 1.9.
La capa del sistema operativo representa la capa de comunicacin entre los procesos que se ejecutan en el mismo y los recursos hardware asociados a la plataforma en cuestin. Tradicionalmente,
en el mundo de los videojuegos los sistemas operativos se compilan con el propio juego para producir
un ejecutable. Sin embargo, las consolas de ltima
[15]
Subsistema
de juego
Networking
Motor de
rendering
Herramientas
de desarrollo
Audio
Motor
de fsica
Interfaces
de usuario
Gestor de recursos
Subsistemas principales
R
R
, incluyen un
o Microsoft XBox 360
generacin, como por ejemplo Sony Playstation 3
sistema operativo capaz de controlar ciertos recursos e incluso interrumpir a un juego en
ejecucin, reduciendo la separacin entre consolas de sobremesa y ordenadores personales.
1.2.2.
SDKs
y middlewares
Al igual que ocurre en otros proyectos software, el desarrollo de un motor de juegos se suele apoyar en bibliotecas existentes y SDK para proporcionar una determinada
funcionalidad. No obstante, y aunque generalmente este software est bastante optimizado, algunos desarrolladores prefieren personalizarlo para adaptarlo a sus necesidades
particulares, especialmente en consolas de sobremesa y porttiles.
[16]
Captulo 1 :: Introduccin
10 http://www.sgi.com/tech/stl/
11 http://http://www.opengl.org/
12 http://www.havok.com
13 http://www.ode.org
[17]
Algunos ejemplos representativos de mdulos incluidos en esta capa son las bibliotecas de manejo de hijos o los wrappers o envolturas sobre alguno de los mdulos de la capa
superior, como el mdulo de deteccin de colisiones o el responsable de la parte grfica.
[18]
Captulo 1 :: Introduccin
Recurso
esqueleto
Recurso
colisin
Mundo
Etc
Recurso
modelo 3D
Recurso
textura
Recurso
material
Recurso
fuente
Gestor de recursos
Figura 1.10: Visin conceptual del gestor de recursos y sus entidades asociadas. Esquema adaptado de la arquitectura propuesta en [5].
el caso particular de Ogre 3D, la clase Ogre::ResourceManager hereda de dos clases, ResourceAlloc y Ogre::ScriptLoader, con el objetivo de unificar completamente las diversas
gestiones. Por ejemplo, la clase Ogre::ScriptLoader posibilita la carga de algunos recursos, como los materiales, mediante scripts y, por ello, Ogre::ResourceManager hereda de
dicha clase.
Ogre::ScriptLoader
ResourceAlloc
Ogre::TextureManager
Ogre::SkeletonManager
Ogre::MeshManager
Ogre::ResourceManager
Ogre::MaterialManager
Ogre::GPUProgramManager
Ogre::FontManager
Ogre::CompositeManager
Figura 1.11: Diagrama de clases asociado al gestor de recursos de Ogre 3D, representado por la clase
Ogre::ResourceManager.
[19]
Front end
Efectos visuales
Motor de rendering
Figura 1.12: Visin conceptual de la arquitectura general de un motor de rendering. Esquema simplificado de
la arquitectura discutida en [5].
[20]
Captulo 1 :: Introduccin
[21]
[22]
Captulo 1 :: Introduccin
[23]
1.2.10.
Networking y multijugador
1.2.11.
Subsistema de juego
[24]
Captulo 1 :: Introduccin
Este subsistema sirve tambin como capa de aislamiento entre las capas de ms bajo
nivel, como por ejemplo la de rendering, y el propio funcionamiento del juego. Es decir,
uno de los principales objetivos de diseo que se persiguen consiste en independizar la
lgica del juego de la implementacin subyacente. Por ello, en esta capa es bastante comn encontrar algn tipo de sistema de scripting o lenguaje de alto nivel para definir, por
ejemplo, el comportamiento de los personajes que participan en el juego.
La capa relativa al subsistema de juego maneja conceptos como el mundo del juego, el cual se
refiere a los distintos elementos que forman parte
del mismo, ya sean estticos o dinmicos. Los tipos de objetos que forman parte de ese mundo se
suelen denominar modelo de objetos del juego [5].
Este modelo proporciona una simulacin en tiempo
real de esta coleccin heterognea, incluyendo
Diseando juegos
Los diseadores de los niveles de un
juego, e incluso del comportamiento de
los personajes y los NPCs, suelen dominar perfectamente los lenguajes de
script, ya que son su principal herramienta para llevar a cabo su tarea.
Sistema de scripting
Objetos
estticos
Objetos
dinmicos
Simulacin
basada en agentes
Sistema de
eventos
Subsistema de juego
Figura 1.13: Visin conceptual de la arquitectura general del subsistema de juego. Esquema simplificado de la
arquitectura discutida en [5].
El modelo de objetos del juego est intimamente ligado al modelo de objetos software
y se puede entender como el conjunto de propiedades del lenguaje, polticas y convenciones utilizadas para implementar cdigo utilizando una filosofa de orientacin a objetos.
As mismo, este modelo est vinculado a cuestiones como el lenguaje de programacin
empleado o a la adopcin de una poltica basada en el uso de patrones de diseo, entre
otras.
[25]
1.2.12.
Audio
[26]
1.2.13.
Captulo 1 :: Introduccin
Por encima de la capa de subsistema de juego y otros componentes de ms bajo nivel se sita la capa de subsistemas especficos de juego, en la que se integran aquellos
mdulos responsables de ofrecer las caractersticas propias del juego. En funcin del tipo
de juego a desarrollar, en esta capa se situarn un mayor o menor nmero de mdulos,
como por ejemplo los relativos al sistema de cmaras virtuales, mecanismos de IA especficos de los personajes no controlados por el usuario (NPCs), aspectos de renderizados
especficos del juego, sistemas de armas, puzzles, etc.
Idealmente, la lnea que separa el motor de juego y el propio juego en cuestin estara
entre la capa de subsistema de juego y la capa de subsistemas especficos de juego.
[28]
2.2.
[29]
[30]
Existen muchos compiladores comerciales de C++ como Borland C++, Microsoft Visual C++. Sin embargo, GCC es un compilador libre y gratuito que soporta C/C++ (entre
otros lenguajes) y es ampliamente utilizado en muchos sectores de la informtica: desde
la programacin grfica a los sistemas empotrados.
Obviamente, existen grandes diferencias entre las implementaciones de los compiladores de C++ disponibles. Estas diferencias vienen determinadas por el contexto de
aplicacin (por ejemplo, para sistemas empotrados) o por el propio fabricante. Sin embargo, es posible extraer una estructura funcional comn como muestra la figura 2.3, que
representa las fases de compilacin en las que, normalmente, est dividido el proceso de
compilacin.
[31]
Enlazador
A medida que los programas crecen se hace necesario poder separarlos en mdulos, es
decir, unidades independientes con sentido en el dominio de la aplicacin y relacionadas
unas con otras. Esto permite que un gran proyecto pueda ser ms mantenible y manejable
que si no existiera forma alguna de dividir funcionalmente un programa. Los mdulos
pueden tener dependencias entre s (por ejemplo, el mdulo A necesita B y C para funcionar) y la comprobacin y resolucin de estas dependencias corren a cargo del enlazador.
El enlazador toma como entrada el cdigo objeto.
Bibliotecas
Una de las principales ventajas del software es la reutilizacin del cdigo. Normalmente, los problemas pueden resolverse utilizando cdigo ya escrito anteriormente y la
reutilizacin del mismo se vuelve un aspecto clave para el tiempo de desarrollo del producto. Las bibliotecas ofrecen una determinada funcionalidad ya implementada para que
sea utilizada por programas. Las bibliotecas se incorporan a los programas durante el
proceso de enlazado.
Las bibliotecas pueden enlazarse contra el programa de dos formas:
Estticamente: en tiempo de enlazado, se resuelven todas las dependencias y smbolos que queden por definir y se incorpora al ejecutable final.
La principal ventaja de utilizar enlazado esttico es que el ejecutable puede considerarse standalone y es completamente independiente. El sistema donde vaya a
ser ejecutado no necesita tener instaladas bibliotecas externas de antemano. Sin
embargo, el cdigo ejecutable generado tiene mayor tamao.
Dinmicamente: en tiempo de enlazado slo se comprueba que ciertas dependencias y smbolos estn definidos, pero no se incorpora al ejecutable. Ser en tiempo
de ejecucin cuando se realizar la carga de la biblioteca en memoria.
El cdigo ejecutable generado es mucho menor, pero el sistema debe tener la biblioteca previamente instalada.
En sistemas GNU/Linux, las bibliotecas ya compiladas suelen encontrarse en /usr/lib
y siguen un convenio para el nombre: libnombre. Las bibliotecas dinmicas tienen extensin .so (shared object) y las estticas .a.
distcc
La compilacin modular y por fases de
GCC permite que herramientas como
distcc puedan realizar compilaciones
distribuidas en red y en paralelo.
[32]
Preprocesador
Compilacin
Ensamblador
Enlazador
Cdigo
preprocesado
Cdigo
ensamblador
Cdigo
objeto
Fichero
de salida
01001011110
...
...
01100011100
...
...
01001011110
...
...
01100011100
...
...
int fun()
...
...
int main (...)
...
...
return 0;
}
mov r1,r3
...
...
add r3,r2,r1
...
...
Preprocesador
El preprocesamiento es la primera transformacin que sufre un programa en C/C++.
Se lleva a cabo por el GNU CPP y, entre otras, realiza las siguientes tareas:
Inclusin efectiva del cdigo incluido por la directiva #include.
Resuelve de las directivas #ifdef/#ifndef para la compilacin condicional.
Sustitucin efectiva de todas las directivas de tipo #define.
La opcin de -E de GCC detiene la compilacin justo despus del preprocesamiento.
El preprocesador se puede invocar directamente utilizando la orden cpp. Como ejercicio
se reserva al lector observar qu ocurre al invocar al preprocesador con el siguiente fragmento de cdigo. Se realizan comprobaciones lexcas, sintticas o semnticas? Utiliza
los parmetros que ofrece el programa para definir la macro DEFINED_IT.
[33]
#include <iostream>
#define SAY_HELLO "Hi, world!"
#ifdef DEFINED_IT
#warning "If you see this message, you DEFINED_IT"
#endif
using namespace std;
Code here??
int main() {
cout << SAY_HELLO << endl;
return 0;
}
Compilacin
El cdigo fuente, una vez preprocesado, se compila a lenguaje ensamblador, es decir,
a una representacin de bajo nivel del cdigo fuente. Originalmente, la sintaxis de este
lenguaje es la de AT&T pero desde algunas versiones recientes tambin se soporta la
sintaxis de Intel.
Entre otras muchas operaciones, en la compilacin se realizan las siguientes operaciones:
Anlisis sintctico y semntico del programa. Pueden ser configurados para obtener
diferentes mensajes de advertencia (warnings) a diferentes niveles.
Comprobacin y resolucin de smbolos y dependencias a nivel de declaracin.
Realizar optimizaciones.
Utilizando GCC con la opcin -S puede detenerse el proceso de compilacin hasta la
generacin del cdigo ensamblador. Como ejercicio, se propone cambiar el cdigo fuente
anterior de forma que se pueda construir el correspondiente en ensamblador.
GCC proporciona diferentes niveles de optimizaciones (opcin -O). Cuanto mayor
es el nivel de optimizacin del cdigo resultante, mayor es el tiempo de compilacin pero suele hacer ms eficiente el cdigo de salida. Por ello, se recomienda no
optimizar el cdigo durante las fases de desarrollo y slo hacerlo en la fase de distribucin/instalacin del software.
Ensamblador
Una vez se ha obtenido el cdigo ensamblador, GNU Assembler es el encargado de
realizar la traduccin a cdigo objeto de cada uno de los mdulos del programa. Por
defecto, el cdigo objeto se genera en archivos con extensin .o y la opcin -c de GCC
permite detener el proceso de compilacin en este punto.
GNU Assembler forma parte de la distribucin GNU Binutils y se corresponde con el
programa as. Como ejercicio, se propone al lector modificar el cdigo ensamblador obtenido en la fase anterior sustituyendo el mensaje original "Hi, world" por "Hola, mundo".
Intenta generar el cdigo objeto asociado utilizando directamente el ensamblador (no
GCC).
[34]
Enlazador
GNU Linker tambin forma parte de la distribucin GNU Binutils y se corresponde
con el programa ld. Con todos los archivos objetos el enlazador (linker) es capaz de
generar el ejecutable o cdigo binario final. Algunas de las tareas que se realizan en el
proceso de enlazado son las siguientes:
2.2.4. Ejemplos
Como se ha mostrado, el proceso de compilacin est compuesto por varias fases bien
diferenciadas. Sin embargo, con GCC se integra todo este proceso de forma que, a partir
del cdigo fuente se genere el binario final.
En esta seccin se mostrarn ejemplos en los que se crea un ejecutable al que, posteriormente, se enlaza con una biblioteca esttica y otra dinmica.
Compilacin de un ejecutable
Como ejemplo de ejecutable se toma el siguiente programa.
#include <iostream>
2
3
4
5
class Square {
private:
int side_;
7
8
9
public:
Square(int side_length) : side_(side_length) { };
int getArea() const { return side_*side_; };
10
11
12
};
13
14
int main () {
15
Square square(5);
16
17
18
[35]
En C++, los programas que generan un ejecutable deben tener definida la funcin
que ser el punto de entrada de la ejecucin. El programa es trivial: se define una
clase Square que representa a un cuadrado. sta implementa un mtodo getArea() que
devuelve el rea del cuadrado.
Suponiendo que el archivo que contiene el cdigo fuente se llama main.cpp, para construir el binario utilizaremos g++, el compilador de C++ que se incluye en GCC. Se podra
utilizar gcc y que se seleccionara automticamente el compilador. Sin embargo, es una
buena prctica utilizar el compilador correcto:
main,
class Square {
private:
int side_;
public:
Square(int side_length);
int getArea() const;
};
#include "Square.h"
Square::Square (int side_length) : side_(side_length)
{ }
int
Square::getArea() const
{
return side_*side_;
}
[36]
#include <iostream>
#include "Square.h"
using namespace std;
int main () {
Square square(5);
cout << "Area: " << square.getArea() << endl;
return 0;
}
Para construir el programa, se debe primero construir el cdigo objeto del mdulo
y aadirlo a la compilacin de la funcin principal main. Suponiendo que el archivo de
cabecera se encuentra en un directorio llamado headers, la compilacin puede realizarse
de la siguiente manera:
$ g++ -Iheaders -c Square.cpp
$ g++ -Iheaders -c main.cpp
$ g++ Square.o main.o -o main
Con la opcin -I, que puede aparecer tantas veces como sea necesario, se puede aadir
rutas donde se buscarn las cabeceras. Ntese que, por ejemplo, en main.cpp se incluyen
las cabeceras usando los smbolos <> y . Se recomienda utilizar los primeros para el caso
en que las cabeceras forman parte de una API pblica (si existe) y deban ser utilizadas
por otros programas. Por su parte, las comillas se suelen utilizar para cabeceras internas
al proyecto. Las rutas por defecto son el directorio actual . para las cabeceras incluidas
con y para el resto el directorio del sistema (normalmente, /usr/include).
Como norma general, una buena costumbre es generar todos los archivos de cdigo
objeto de un mdulo y aadirlos a la compilacin con el programa principal.
#ifndef FIGURE_H
#define FIGURE_H
class Figure {
public:
virtual float getArea() const = 0;
};
#endif
#include <Figure.h>
1
2
3
4
#include "Square.h"
#include <Figure.h>
class Triangle : public Figure {
private:
float base_;
float height_;
public:
Triangle(float base_, float height_);
float getArea() const;
};
#include "Triangle.h"
Triangle::Triangle (float base, float height) : base_(base), height_(height) { }
float Triangle::getArea() const { return (base_*height_)/2; }
[37]
[38]
1
2
3
4
5
6
7
8
9
10
#include <Figure.h>
class Square : public Figure {
private:
float side_;
public:
Square(float side_length);
float getArea() const;
};
#include <cmath>
#include "Circle.h"
Circle::Circle(float radious) : radious_(radious) { }
float Circle::getArea() const { return radious_*(M_PI*M_PI); }
figures,
ar es un programa que permite, entre otra mucha funcionalidad, empaquetar los archivos de cdigo objeto y generar un ndice para crear un biblioteca. Este ndice se incluye
en el mismo archivo generado y mejora el proceso de enlazado y carga de la biblioteca.
A continuacin, se muestra el programa principal que hace uso de la biblioteca:
#include <iostream>
#include <Square.h>
#include <Triangle.h>
#include <Circle.h>
using namespace std;
int main () {
Square square(5);
Triangle triangle(5,10);
Circle circle(10);
cout << "Square area: " << square.getArea() << endl;
cout << "Triangle area: " << triangle.getArea() << endl;
cout << "Circle area: " << circle.getArea() << endl;
return 0;
}
[39]
g++
g++
g++
g++
Como se puede ver, se utiliza GCC directamente para generar la biblioteca dinmica.
La compilacin y enlazado con el programa principal se realiza de la misma forma que
en el caso del enlazado esttico. Sin embargo, la ejecucin del programa principal es
diferente. Al tratarse de cdigo objeto que se cargar en tiempo de ejecucin, existen una
serie de rutas predefinadas donde se buscarn las bibliotecas. Por defecto, son las mismas
que para el proceso de enlazado.
Tambin es posible aadir rutas modificando la variable LD_LIBRARY_PATH:
$ LD_LIBRARY_PATH=. ./main
objdump:
similar a objdump pero especfico para los archivos objeto para plataformas
compatibles con el formato binario Executable and Linkable Format (ELF).
1 Ms
informacin en http://www.gnu.org/software/binutils/
[40]
Para depurar no se debe hacer uso de las optimizaciones. stas pueden generar cdigo que nada tenga que ver con el original.
[41]
#include <iostream>
using namespace std;
class Test {
int _value;
public:
void setValue(int a) { _value = a; }
int getValue() { return _value; }
};
float functionB(string str1, Test* t) {
cout << "Function B: " << str1 << ", " << t->getValue() << endl;
return 3.14;
}
int functionA(int a) {
cout << "Function A: " << a << endl;
Test* test = NULL; /** ouch! **/
test->setValue(15);
cout << "Return B: " << functionB("Hi", test) << endl;
return 5;
}
int main() {
cout << "Main start" << endl;
cout << "Return A: " << functionA(24) << endl;
return 0;
}
Una violacin de segmento (segmentation fault) es uno de los errores lgicos tpicos
de los lenguajes como C++. El problema es que se est accediendo a una zona de la memoria que no ha sido reservada por el programa de forma explcita (por ejemplo, usando el
operador new), por lo que el sistema operativo interviene denegando ese acceso indebido.
A continuacin, se muestra cmo iniciar una sesin de depuracin con GDB para
encontrar el origen del problema:
$ gdb main
...
Reading symbols from ./main done.
(gdb)
[42]
Como se puede ver, GDB ha cargado el programa, junto con los smbolos de depuracin necesarios, y ahora se ha abierto una lnea de rdenes donde el usuario puede
especificar sus acciones.
Con los programas que fallan en tiempo de ejecucin se puede generar un archivo de
core, es decir, un fichero que contiene un volcado de la memoria en el momento en
que ocurri el fallo. Este archivo puede ser cargado en una sesin de GDB para ser
examinado usando la opcin -c.
Examinando el contenido
Para comenzar la ejecucin del programa se puede utilizar la orden start:
(gdb) start
Temporary breakpoint 1 at 0x400d31: file main.cpp, line 26.
Starting program: main
Temporary breakpoint 1, main () at main.cpp:26
26
cout << "Main start" << endl;
(gdb)
Todas las rdenes de GDB pueden escribirse utilizando su abreviatura. Ej: run = r.
Como el resto de rdenes, list acepta parmetros que permiten ajustar su comportamiento.
Las rdenes que permiten realizar una ejecucin controlada son las siguientes:
step (s): ejecuta la instruccin actual y salta a la inmediatamente siguiente sin man-
[43]
next (n):
En este punto se puede hacer uso de las rdenes para mostrar el contenido del parmetro a de la funcin functionA():
(gdb) print a
$1 = 24
(gdb) print &a
$2 = (int *) 0x7fffffffe1bc
La ejecucin est detenida en la lnea 19 donde un comentario nos avisa del error.
Se est creando un puntero con el valor NULL. Posteriormente, se invoca un mtodo sobre un objeto que no est convenientemente inicializado, lo que provoca la violacin de
segmento:
(gdb) next
20
test->setValue(15);
(gdb)
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400df2 in Test::setValue (this=0x0, a=15) at main.cpp:8
8
void setValue(int a) { _value = a; }
[44]
int functionA(int a) {
cout << "Function A: " << a << endl;
Test* test = new Test();
test->setValue(15);
cout << "Return B: " << functionB("Hi", test) << endl;
return 5;
}
Breakpoints
La ejecucin paso a paso es una herramienta til para una depuracin de grano fino.
Sin embargo, si el programa realiza grandes iteraciones en bucles o es demasiado grande,
puede ser un poco incmodo (o inviable). Si se tiene la sospecha sobre el lugar donde est
el problema se pueden utilizar puntos de ruptura o breakpoints que permite detener el
flujo de ejecucin del programa en un punto determinado por el usuario y, as, dar tiempo
para examinar cul es el estado de ciertas variables.
Con el ejemplo ya arreglado, se configura un breakpoint en la funcin functionB() y
otro en la lnea 28 con la orden break. A continuacin, se ejecuta el programa hasta que
se alcance el breakpoint con la orden run (r):
(gdb) break functionB
Breakpoint 1 at 0x400c15: file main.cpp, line 13.
(gdb) break main.cpp:28
Breakpoint 2 at 0x400ddb: file main.cpp, line 28.
(gdb) run
Starting program: main
Main start
Function A: 24
Breakpoint 1, functionB (str1=..., t=0x602010) at gdb-fix.cpp:13
13
cout << "Function B: " << str1 << ", " << t->getValue() << endl;
(gdb)
No hace falta escribir todo!. Utiliza TAB para completar los argumentos de una orden.
Con la orden continue (c) la ejecucin avanza hasta el siguiente punto de ruptura (o
fin del programa):
(gdb) continue
Continuing.
Function B: Hi, 15
Return B: 3.14
Return A: 5
Breakpoint 2, main () at gdb-fix.cpp:28
28
return 0;
(gdb)
Los breakpoint pueden habilitarse, inhabilitarse y/o eliminarse en tiempo de ejecucin. Adems, GDB ofrece un par de estructuras similares tiles para otras situaciones:
[45]
Con up y down se puede navegar por los frames de la pila, y con frame se puede seleccionar uno en concreto:
(gdb) up
#1 0x0000000000400d07 in functionA (a=24) at gdb-fix.cpp:21
21
cout << "Return B: " << functionB("Hi", test) << endl;
(gdb)
#2 0x0000000000400db3 in main () at gdb-fix.cpp:27
27
cout << "Return A: " << functionA(24) << endl;
(gdb) frame 0
#0 functionB (str1=..., t=0x602010) at gdb-fix.cpp:13
13
cout << "Function B: " << str1 << ", " << t->getValue() << endl;
(gdb)
Una vez seleccionado un frame, se puede obtener toda la informacin del mismo, adems de modificar las variables y argumentos:
Invocar funciones
La orden call se puede utilizar para invocar funciones y mtodos.
(gdb) print *t
$1 = {_value = 15}
(gdb) call t->setValue(1000)
(gdb) print *t
$2 = {_value = 1000}
(gdb)
[47]
El principio del fichero Makefile se reserva para definiciones de variables que van a
ser utilizadas a lo largo del mismo o por otros Makefiles (para ello puede utilizar export).
A continuacin, se definen el conjunto de reglas de construccin para cada uno de los
archivos que se pretende generar a partir de sus dependencias. Por ejemplo, el archivo
target1 necesita que existan dependency1, dependency2, etc. action1 y action2 indican
cmo se construye. La siguiente regla tiene como objetivo dependency1 e igualmente se
especifica cmo obtenerlo a partir de dependency3.
Las acciones de una regla van siempre van precedidas de un carcter de tabulado.
Existen algunos objetivos especiales como all, install y clean que sirven como regla
de partida inicial, para instalar el software construido y para limpiar del proyecto los
archivos generados, respectivamente. Este tipo de objetivos son denominados objetivos
ficticios porque no generan ningn archivo y se ejecutarn siempre que sean invocados y
no exista ningn archivo con su nombre.
Tomando como ejemplo la aplicacin que hace uso de la biblioteca dinmica, el siguiente listado muestra el Makefile que generara tanto el programa ejecutable como la
biblioteca esttica:
Listado 2.17: Makefile bsico
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
all: main
main: main.o libfigures.a
g++ main.o -L. -lfigures -o main
main.o: main.cpp
g++ -Iheaders -c main.cpp
libfigures.a: Square.o Triangle.o Circle.o
ar rs libfigures.a Square.o Triangle.o Circle.o
Square.o: Square.cpp
g++ -Iheaders -fPIC -c Square.cpp
Triangle.o: Triangle.cpp
g++ -Iheaders -fPIC -c Triangle.cpp
Circle.o: Circle.cpp
g++ -Iheaders -fPIC -c Circle.cpp
clean:
rm -f *.o *.a main
Con ello, se puede construir el proyecto utilizando make [objetivo]. Si no se proporciona objetivo se toma el primero en el archivo. De esta forma, se puede construir todo el
proyecto, un archivo en concreto o limpiarlo completamente. Por ejemplo, para construir
y limpiar todo el proyecto se ejecutaran las siguientes rdenes:
$ make
$ make clean
make
[48]
Make se caracteriza por ofrecer gran versatilidad en su lenguaje. Las variables automticas contienen valores que dependen del contexto de la regla donde se aplican y permiten definir reglas genricas. Por su parte, los patrones permiten generalizar las reglas utilizando el nombre los
archivos generados y los fuentes.
A continuacin se presenta una versin mejorada del anterior Makefile haciendo uso
de variables, variables automticas y patrones:
Listado 2.18: Makefile con variables automticas y patrones
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-f.
Variables automticas $@, $<, $: se utiliza para hacer referencia al nombre del objetivo de la regla, al nombre de la primera dependencia y a la lista de todas las
dependencias de la regla, respectivamente.
Regla con patrn: en lnea 12 se define una regla genrica a travs de un patrn en
el que se define cmo generar cualquier archivo objeto .o, a partir de un archivo
fuente .cpp.
Como ejercicio se plantean las siguientes cuestiones: qu ocurre si una vez construido el proyecto se modifica algn fichero .cpp? Y si se modifica una cabecera .h?
Se podra construir una regla con patrn genrica para construir la biblioteca esttica?
Cmo lo haras?
Reglas implcitas
Como se ha dicho, GNU Make es ampliamente utilizado para la construccin de proyectos software donde estn implicados diferentes procesos de compilacin y generacin
de cdigo. Por ello, proporciona las llamadas reglas implcitas de forma que si el usuario
define ciertas variables, Make puede deducir cmo construir los objetivos en base a los
nombres de los archivos, lenguaje utilizado, etc. Make es muy listo!.
[49]
CC=g++
CXXFLAGS=-Iheaders
LDFLAGS=-L.
LDLIBS=-lfigures
LIB_OBJECTS=Square.o Triangle.o Circle.o
all: libfigures.a main
libfigures.a: $(LIB_OBJECTS)
$(AR) r $@ $^
clean:
$(RM) *.o *.a main
Como se puede ver, Make puede generar automticamente los archivos objeto .o a
partir de la coincidencia con el nombre del fichero fuente (que es lo habitual). Por ello,
no es necesario especificar cmo construir los archivos .o de la biblioteca, ni siquiera la
regla para generar main ya que asume de que se trata del ejecutable (al existir un fichero
llamado main.cpp).
Las variables de usuario que se han definido permiten configurar los flags de compilacin que se van a utilizar en las reglas explcitas. As:
CC:
CXXFLAGS:
flags para el enlazador (ver seccin 2.2.3). Aqu se definen las rutas de
bsqueda de las bibliotecas estticas y dinmicas.
LDLIBS: en esta variable se especifican las opciones de enlazado. Normalmente, basta con utilizar la opcin -l con las bibliotecas necesarias para generar el ejecutable.
Funciones
GNU Make proporciona un conjunto de funciones que pueden ser de gran ayuda a
la hora de construir los Makefiles. Muchas de las funciones estn diseadas para el tratamiento de cadenas, ya que se suelen utilizar para manipular los nombres de archivos.
Sin embargo, existen muchas otras como para realizar ejecucin condicional, bucles o
ejecutar rdenes de consola. En general, las funciones tienen el siguiente formato:
$(nombre arg1,arg2,arg3,...)
Las funciones se pueden utilizar en cualquier punto del Makefile, desde las acciones
de una regla hasta en la definicin de un variable. En el siguiente listado se muestra el uso
de algunas de estas funciones:
[50]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CC=g++
ifeq ($(DEBUG),yes)
CXXFLAGS=-Iheader -Wall -ggdb
else
CXXFLAGS=-Iheader -O2
endif
LDFLAGS=-L.
LDLIBS=-lfigures
LIB_OBJECTS=Square.o Triangle.o Circle.o
all: libfigures.a main
$(info All done!)
libfigures.a: $(LIB_OBJECTS)
$(AR) r $@ $^
$(warning Compiled objects from $(foreach OBJ,
$(LIB_OBJECTS),
$(patsubst %.o, %.cpp,$(OBJ))))
clean:
$(RM) *.o *.a main
$(shell find -name *~ -delete)
Funciones de bucles: foreach permite aplicar una funcin a cada valor de una lista.
Este tipo de funciones devuelven una lista con el resultado.
Funciones de tratamiento de texto: por ejemplo, patsubst toma como primer parmetro un patrn que se comprobar por cada OBJ. Si hay matching, ser sustituido
por el patrn definido como segundo parmetro. En definitiva, cambia la extensin
.o por .cpp.
Funciones de log: info, warning, error, etc. permiten mostrar texto a diferente nivel
de severidad.
Funciones de consola: shell es la funcin que permite ejecutar rdenes en un
terminal. La salida es til utilizarla como entrada a una variable.
Ms informacin
GNU Make es una herramienta que est en continuo crecimiento y esta seccin slo
ha sido una pequea presentacin de sus posibilidades. Para obtener ms informacin
sobre las funciones disponibles, otras variables automticas y objetivos predefinidos se
recomiendo utilizar el manual en lnea de Make2 , el cual siempre se encuentra actualizado.
2 http://www.gnu.org/software/make/manual/make.html
2.3.
[51]
Los proyectos software pueden ser realizados por varios equipos de personas, con
formacin y conocimientos diferentes, que deben colaborar entre s. Adems, una componente importante del proyecto es la documentacin que se genera durante el transcurso
del mismo, es decir, cualquier documento escrito o grfico que permita entender mejor
los componentes del mismo y, por ello, asegure una mayor mantenibilidad para el futuro
(manuales, diagramas, especificaciones, etc.).
La gestin del proyecto es un proceso transversal al resto de procesos y tareas y se
ocupa de la planificacin y asignacin de los recursos de los que se disponen. Se trata
de un proceso dinmico, ya que debe adaptarse a los diferentes cambios e imprevistos
que puedan surgir durante el desarrollo. Para detectar estos cambios a tiempo, dentro
de la gestin del proyecto se realizan tareas de seguimiento que consisten en registrar y
notificar errores y retrasos en las diferentes fases y entregas.
Existen muchas herramientas que permiten crear entornos colaborativos que facilitan
el trabajo tanto a desarrolladores como a los jefes de proyecto, los cuales estn ms centrados en las tareas de gestin. En esta seccin se presentan algunos entornos colaborativos
actuales, as como soluciones especficas para un proceso concreto.
[52]
Los VCS centralizados como CVS o Subversion se basan en que existe un nodo servidor con el que todos los clientes conectan para obtener los archivos, subir modificaciones,
etc. La principal ventaja de este esquema reside en su sencillez: las diferentes versiones
del proyecto estn nicamente en el servidor central, por lo que los posibles conflictos
entre las modificaciones de los clientes pueden detectarse y gestionarse ms fcilmente.
Sin embargo, el servidor es un nico punto de fallo y en caso de cada, los clientes quedan
aislados.
Por su parte, en los VCS distribuidos como Mercurial o Git, cada cliente tiene un
repositorio local al nodo en el que se suben los diferentes cambios. Los cambios pueden
agruparse en changesets, lo que permite una gestin ms ordenada. Los clientes actan
de servidores para el resto de los componentes del sistema, es decir, un cliente puede
descargarse una versin concreta de otro cliente.
Esta arquitectura es tolerante a fallos y permite a los clientes realizar cambios sin
necesidad de conexin. Posteriormente, pueden sincronizarse con el resto. An as, un
VCS distribuido puede utilizarse como uno centralizado si se fija un nodo como servidor,
pero se perderan algunas posibilidades que este esquema ofrece.
Subversion
Subversion (SVN) es uno de los VCS centralizado ms utilizado, probablemente, debido a su sencillez en el uso. Bsicamente, los clientes tienen accesible en un servidor
todo el repositorio y es ah donde se envan los cambios.
Para crear un repositorio, en el servidor se puede ejecutar la siguiente orden:
$ svnadmin create /var/repo/myproject
En /var/repo/myproject se crear un rbol de directorios que contendr toda la informacin necesaria para mantener la informacin de SVN. Una vez hecho esto es necesario
hacer accesible este directorio a los clientes. En lo que sigue, se supone que los usuarios
tienen acceso a la mquina a travs de una cuenta SSH, aunque se pueden utilizar otros
mtodos de acceso. Es recomendable que el acceso al repositorio est controlado. HTTPS
o SSH son buenas opciones de mtodos de acceso.
Inicialmente, los clientes pueden descargarse el repositorio por primera vez utilizando
la orden checkout:
[53]
Esto ha creado un directorio myproject con el contenido del repositorio. Una vez descargado, se pueden realizar todos los cambios que se deseen y, posteriormente, subirlos al
repositorio. Por ejemplo, aadir archivos y/o directorios:
$
$
$
$
A
A
A
mkdir doc
echo "This is a new file" > doc/new_file
echo "Other file" > other_file
svn add doc other_file
doc
doc/new_file
other_file
La operacin add indica qu archivos y directorios han sido seleccionados para ser
aadidos al repositorio (marcados con A). Esta operacin no sube efectivamente los archivos al servidor. Para subir cualquier cambio se debe hacer un commit:
$ svn commit
3 El
[54]
La marca C indica que other_file queda en conflicto y que debe resolverse manualmente. Para resolverlo, se debe editar el archivo donde Subversion marca las diferencias
con los smbolos < y >.
Tambin es posible tomar como solucin el revertir los cambios realizados por user1
y, de este modo, aceptar los de user2:
$ svn revert other_file
$ svn commit
Committed revision X+2.
new_file,
aceptando los
Mercurial
Como se ha visto, en los VCS centralizados como Subversion no se permite, por
ejemplo, que los clientes hagan commits si no estn conectados con el servidor central.
Los VCS como Mercurial (HG), permiten que los clientes tengan un repositorio local, con
su versin modificada del proyecto y la sincronizacin del mismo con otros servidores
(que pueden ser tambin clientes).
Para crear un repositorio Mercurial, se debe ejecutar
lo siguiente:
$ hg init /home/user1/myproyect
$ hg clone ssh://user2@host//home/user1/myproyect
A partir de este instante, user2 tiene una versin inicial del proyecto extrada a partir de la del usuario user1.
De forma muy similar a Subversion, con la orden add se
pueden aadir archivos y directorios.
Mientras que en el modelo de Subversion, los clientes
hacen commit y update para subir cambios y obtener la ltima versin, respectivamente;
en Mercurial es algo ms complejo, ya que existe un repositorio local. Como se muestra
en la figura 2.8, la operacin commit (3) sube los cambios a un repositorio local que cada
cliente tiene.
Cada commit se considera un changeset, es decir, un conjunto de cambios agrupados
por un mismo ID de revisin. Como en el caso de Subversion, en cada commit se pedir
una breve descripcin de lo que se ha modificado.
Una vez hecho todos commits, para llevar estos cambios a un servidor remoto se debe
ejecutar la orden de push (4). Siguiendo con el ejemplo, el cliente user2 lo enviar por
defecto al repositorio del que hizo la operacin clone.
[55]
El sentido inverso, es decir, traerse los cambios del servidor remoto a la copia local,
se realiza tambin en 2 pasos: pull (1) que trae los cambios del repositorio remoto al
repositorio local; y update (2), que aplica dichos cambios del repositorio local al directorio
de trabajo. Para hacer los dos pasos al mismo tiempo, se puede hacer lo siguiente:
$ hg pull -u
Para evitar conflictos con otros usuarios, una buena costumbre antes de realizar un
es conveniente obtener los posibles cambios en el servidor con pull y update.
push
Para ver cmo se gestionan los conflictos en Mercurial, supngase que user1 realiza
lo siguiente:
$ echo "A file" > a_file
$ hg add a_file
$ hg commit
[56]
hgview
$ hg pull
adding changesets
adding manifests
adding file changes
added 2 changesets with 1 changes to 1 files (+1 heads)
(run hg heads to see heads, hg merge to merge)
$ hg merge
merging a_file
warning: conflicts during merge.
merging a_file failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
$ hg resolve -a
$ hg commit
$ hg push
Git
Diseado y desarrollado por Linus Torvalds para el
proyecto del kernel Linux, Git es un VCS distribuido que
cada vez es ms utilizado por la comunidad de desarrolladores, sobre todo en el mbito del Software Libre. En
trminos generales, tiene una estructura similar a Mercurial: independencia entre repositorio remotos y locales,
Figura 2.9: Logotipo del proyecto gestin local de cambios, etc.
Git.
Sin embargo, Git es en ocasiones preferido sobre
Mercurial por algunas de sus caractersticas propias:
Eficiencia y rapidez a la hora de gestionar grandes cantidades de archivos. Git no
funciona peor conforme la historia del repositorio crece.
Facilita el desarrollo no lineal, es decir, el programador puede crear ramas locales
e integrar los cambios entre diferentes ramas (tanto remotas como locales) de una
forma simple y con muy poco coste. Git est diseado para que los cambios puedan
ir de rama a rama y ser revisados por diferentes usuarios. Es una herramienta muy
potente de revisin de cdigo.
Diseado como un conjunto de pequeas herramientas, la mayora escritas en C,
que pueden ser compuestas entre ellas para hacer tareas ms complejas. Su estructura general recuerda a las herramientas UNIX como sort, ls, sed, etc. que realizan
tareas muy especficas y concretas y, al mismo tiempo, se pueden componer unas
con otras.
[57]
En general, Git proporciona ms flexibilidad al usuario, permitiendo hacer tareas complejas y de grano fino (como la divisin de un cambio en diferentes cambios) y al mismo
tiempo es eficiente y escalable para proyectos con gran cantidad de archivos y usuarios
concurrentes.
Un repositorio Git est formado, bsicamente, por un conjunto de objetos commit y
un conjunto de referencias a esos objetos llamadas heads. Un commit es un concepto
similar a un changeset de Mercurial y se compone de las siguientes partes:
El conjunto de ficheros que representan al proyecto en un momento concreto.
Referencias a los commits padres.
Un nombre formado a partir del contenido (usando el algoritmo SHA1).
Cada head apunta a un commit y tiene un nombre simblico para poder ser referenciado. Por defecto, todos los respositorios Git tienen un head llamado master. HEAD
(ntese todas las letras en mayscula) es una referencia al head usado en cada instante.
Por lo tanto, en un repositorio Git, en un estado sin cambios, HEAD apuntar al master
del repositorio.
Para crear un repositorio donde sea posible que otros usuarios puedan subir cambios
se utiliza la orden init:
$ git init --bare /home/user1/myproyect
clone
sera la si-
De esta manera, una vez creado un repositorio Git, es posible reconfigurar en la URL
donde se conectarn las rdenes pull y fetch para descargar el contenido. En la figura 2.10
se muestra un esquema general del flujo de trabajo y las rdenes asociadas. Ntese la
utilidad de la orden remote que permite definir el repositorio remoto del que recibir y al
que se enviarn los cambios. origin es el nombre utilizado para el respositorio remoto por
defecto, pero se pueden aadir cuantos sean requeridos.
En Git se introduce el espacio stage (tambin llamado index o cache) que acta como
paso intermedio entre la copia de trabajo del usuario y el repositorio local. Slo se pueden
enviar cambios (commits) al repositorio local si stos estn previamente en el stage. Por
ello, todos los nuevos archivos y/o modificaciones que se realicen deben ser aadidas
al stage antes.
La siguiente secuencia de comandos aaden un archivo nuevo al repositorio local.
Ntese que add slo aade el archivo al stage. Hasta que no se realice commit no llegar a
estar en el repositorio local:
[58]
Servidor
1
4
Cliente
1:
2:
3:
4:
5:
6:
7:
7
5
2
3
rea de
trabajo
$
$
$
#
#
#
#
#
#
example
Ntese como la ltima llamada a status no muestra ningn cambio por subir al repositorio local. Esto significa que la copia de trabajo del usuario est sincronizada con
el repositorio local (todava no se han realizado operaciones con el remoto). Se puede
utilizar reflog para ver la historia:
$ git reflog
2f81676 HEAD@{0}: commit: Test example: initial version
...
diff se utiliza para ver cambios entre commits, ramas, etc. Por ejemplo, la siguiente
orden muestra las diferencias entre master del repositorio local y master del remoto:
$ git diff master origin/master
diff --git a/example b/example
new file mode 100644
index 0000000..ac19bf2
--- /dev/null
+++ b/example
@@ -0,0 +1 @@
+Test example
[59]
Existen herramientas grficas como gitk para entender mejor estos conceptos y visualizarlos durante el proceso de desarrollo y que permiten ver todos los commits y heads en
cada instante.
Para modificar cdigo y subirlo al repositorio local se sigue el mismo procedimiento:
(1) realizar la modificacin, (2) usar add para aadir el archivo cambiado, (3) hacer commit
para subir el cambio al repositorio local. Sin embargo, como se ha dicho anteriormente,
una buena caracterstica de Git es la creacin y gestin de ramas (branches) locales que
permiten hacer un desarrollo en paralelo incluso a nivel local, es decir, sin subir nada al
repositorio central. Esto es muy til ya que, normalmente, en el ciclo de vida de desarrollo
de un programa se debe simultanear tanto la creacin de nuevas caractersticas como
arreglos de errores cometidos. En Git, estas ramas no son ms que referencias a commits,
por lo que son muy ligeras y pueden llevarse de un sitio a otro de forma sencilla.
Como ejemplo, la siguiente secuencia de rdenes crea una rama local a partir del
HEAD, modifica un archivo en esa rama y finalmente realiza un merge con master:
$ git checkout -b "NEW-BRANCH"
Switched to a new branch NEW-BRANCH
$ git branch
* NEW-BRANCH
master
$ emacs example # se realizan las modificaciones
$ git add example
$ git commit -m "remove example"
[NEW-BRANCH 263e915] remove example
1 file changed, 1 insertion(+), 1 deletion(-)
$ git checkout master
Switched to branch master
$ git branch
NEW-BRANCH
* master
$ git merge NEW-BRANCH
Updating 2f81676..263e915
Fast-forward
example | 2 +1 file changed, 1 insertion(+), 1 deletion(-)
Ntese cmo la orden branch muestra la rama actual marcada con el smbolo *. Utilizando las rdenes log y show se pueden listar los commits recientes. Estas rdenes aceptan, adems de identificadores de commits, ramas y rangos temporales de forma que pueden obtenerse gran cantidad de informacin de ellos.
Finalmente, para desplegar los cambios en el repositorio remoto slo hay que utilizar:
$ git push
Git utiliza ficheros como .gitconfig y .gitignore para cargar configuraciones personalizadas e ignorar ficheros a la hora de hacer los commits, respectivamente. Son
muy tiles. Revisa la documentacin de git-config y gitignore para ms informacin.
[60]
[61]
[62]
2.3.3. Documentacin
Uno de los elementos ms importantes que se generan en un proyecto es la documentacin: cualquier elemento que permita entender mejor tanto el proyecto en su totalidad
como sus partes, de forma que facilite el proceso de mantenimiento en el futuro. Adems,
una buena documentacin har ms sencilla la reutilizacin de componentes.
Existen muchos formatos de documentacin que pueden servir para un proyecto software. Sin embargo, muchos de ellos, tales como PDF, ODT, DOC, etc., son formatos
binarios, cuyos cambios no son fciles de seguir, por lo que no son aconsejables para
utilizarlos en un VCS. Adems, utilizando texto plano es ms sencillo crear programas
que automaticen la generacin de documentacin, de forma que se ahorre tiempo en este
proceso.
Por ello, aqu se describen algunas formas de crear documentacin basadas en texto
plano. Obviamente, existen muchas otras y, seguramente, sean tan vlidas como las que
se proponen aqu.
Doxygen
Doxygen es un sistema que permite generar la documentacin utilizando analizadores
de cdigo que averiguan la estructura de mdulos y clases, as como las funciones y los
mtodos utilizados. Adems, se pueden realizar anotaciones en los comentarios del cdigo
que sirven para aadir informacin ms detallada. La principal ventaja es que se vale del
propio cdigo fuente para generar la documentacin. Adems, si se aaden comentarios
en un formato determinado, es posible ampliar la documentacin generada con notas y
aclaraciones sobre las estructuras y funciones utilizadas.
Algunos piensan que el uso de programas como Doxygen es bueno porque obliga
a comentar el cdigo. Otros piensan que no es as ya que los comentarios deben
seguir un determinado formato, dejando de ser comentarios propiamente dichos.
El siguiente fragmento de cdigo muestra una clase en C++ documentada con el formato de Doxygen:
Listado 2.21: Clase con comentarios Doxygen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
This is a test class to show Doxygen format documentation.
*/
class Test {
public:
/// The Test constructor.
/**
\param s the name of the Test.
*/
Test(string s);
/// Start running the test.
/**
\param max maximum time of test delay.
\param silent if true, do not provide output.
\sa Test()
*/
int run(int max, bool silent);
};
[63]
reStructuredText
reStructuredText (RST) es un formato de texto bsico que permite escribir texto plano
aadiendo pequeas anotaciones de formato de forma que no se pierda legibilidad. Existen muchos traductores de RST a otros formatos como PDF (rst2pdf) y HTML (rst2html),
que adems permiten modificar el estilo de los documentos generados.
El formato RST es similar a la sintaxis de los sistema tipo wiki. Un ejemplo de archivo
en RST puede ser el siguiente:
Listado 2.22: Archivo en RST
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
================
The main title
================
This is an example of document in ReStructured Text (RST). You can get
more info about RST format at RST Reference
<http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html>_.
Other section
=============
You can use bullet items:
- Item A
- Item B
And a enumerated list:
1. Number 1
2. Number 2
Tables
-----+--------------+----------+-----------+-----------+
| row 1, col 1 | column 2 | column 3 | column 4 |
+--------------+----------+-----------+-----------+
| row 2
|
|
|
|
+--------------+----------+-----------+-----------+
Images
-----.. image:: gnu.png
:scale: 80
:alt: A title text
Como se puede ver, aunque RST aade una sintaxis especial, el texto es completamente legible. sta es una de las ventajas de RST, el uso de etiquetas de formato que no
ensucian demasiado el texto.
[64]
YAML
YAML (YAML Aint Markup Language)4 es un lenguaje diseado para serializar datos procedentes de aplicaciones en un formato que sea legible para los humanos. Estrictamente, no se trata de un sistema para documentacin, sin embargo, y debido a lo cmodo
de su sintaxis, puede ser til para exportar datos, representar configuraciones, etc. Otra de
sus ventajas es que hay un gran nmero de bibliotecas en diferentes lenguajes (C++, Python, Java, etc.) para tratar informacin YAML. Las libreras permiten automticamente
salvar las estructuras de datos en formato YAML y el proceso inverso: cargar estructuras
de datos a partir del YAML.
En el ejemplo siguiente, extrado de la documentacin oficial, se muestra una factura.
De un primer vistazo, se puede ver qu campos forman parte del tipo de dato factura
tales como invoice, date, etc. Cada campo puede ser de distintos tipo como numrico,
booleano o cadena de caracteres, pero tambin listas (como product) o referencias a otros
objetos ya declarados (como ship-to).
Listado 2.23: Archivo en YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
--- !<tag:clarkevans.com,2002:invoice>
invoice: 34843
date
: 2001-01-23
bill-to: &id001
given : Chris
family : Dumars
address:
lines: |
458 Walkman Dr.
Suite #292
city
: Royal Oak
state
: MI
postal : 48046
ship-to: *id001
product:
- sku
: BL394D
quantity
: 4
description : Basketball
price
: 450.00
- sku
: BL4438H
quantity
: 1
description : Super Hoop
price
: 2392.00
tax : 251.42
total: 4443.52
comments:
Late afternoon is best.
Backup contact is Nancy
Billsmer @ 338-4338.
4 http://www.yaml.org
[65]
[66]
Redmine
Adems de los servicios gratuitos presentados, existe gran variedad de software que
puede ser utilizado para gestionar un proyecto de forma que pueda ser instalado en un
servidor personal y as no depender de un servicio externo. Tal es el caso de Redmine
(vase figura 2.12) que entre las herramientas que proporciona cabe destacar las siguientes
caractersticas:
Permite crear varios proyectos. Tambin es configurable qu servicios se proporcionan en cada proyecto: gestor de tareas, tracking de fallos, sistema de documentacin
wiki, etc.
Integracin con repositorios, es decir, el cdigo es accesible a travs de Redmine y
se pueden gestionar tareas y errores utilizando los comentarios de los commits.
Gestin de usuarios que pueden utilizar el sistema y sus polticas de acceso.
Est construido en Ruby y existe una amplia variedad de plugins que aaden funcionalidad extra.
10 http://code.google.com
[68]
El origen de C++ est ligado al origen de C, ya que C++ est construido sobre C. De
hecho, C++ es un superconjunto de C y se puede entender como una versin extendida y
mejorada del mismo que integra la filosofa de la POO y otras mejoras, como por ejemplo la inclusin de un conjunto amplio de bibliotecas. Algunos autores consideran que
C++ surgi debido a la necesidad de tratar con programas de mayor complejidad, siendo
impulsado en gran parte por la POO.
C++ fue diseado por Bjarne Stroustrup1 en 1979. La idea de Stroustrup fue aadir
nuevos aspectos y mejoras a C, especialmente en relacin a la POO, de manera que un
programador de C slo que tuviera que aprender aquellos aspectos relativos a la OO.
En el caso particular de la industria del videojuego,
C++ se puede considerar como el estndar de facto debido principalmente a su eficiencia y portabilidad. C++ es
una de los pocos lenguajes que posibilitan la programacin de alto nivel y, de manera simultnea, el acceso a los
recursos de bajo nivel de la plataforma subyacente. Por
lo tanto, C++ es una mezcla perfecta para la programacin de sistemas y para el desarrollo de videojuegos. Una
de las principales claves a la hora de manejarlo eficienteFigura 3.1: Bjarne Stroustrup, crea- mente en la industria del videojuego consiste en encontrar
dor del lenguaje de programacin el equilibrio adecuado entre eficiencia, fiabilidad y manC++ y personaje relevante en el m- tenibilidad [3].
bito de la programacin.
1 http://www2.research.att.com/~bs/
[69]
elementos de un programa. Los espacios de nombres siguen la misma filosofa que los
paquetes en Java y tienen como objetivo organizar los programas. El hecho de utilizar
un espacio de nombres permite acceder a sus elementos y funcionalidades sin tener que
especificar a qu espacio pertenecen.
[70]
int *ip;
[71]
#include <iostream>
using namespace std;
int main () {
char s[20] = "hola mundo";
char *p;
int i;
for (p = s, i = 0; p[i]; i++)
p[i] = toupper(p[i]);
cout << s << endl;
return 0;
}
El caso tpico tiene lugar cuando un puntero apunta a algn lugar de memoria que no
debe, modificando datos a los que no debera apuntar, de manera que el programa muestra resultados indeseables posteriormente a su ejecucin inicial. En estos casos, cuando se
detecta el problema, encontrar la evidencia del fallo no es una tarea trivial, ya que inicialmente puede que no exista evidencia del puntero que provoc dicho error. A continuacin
se muestran algunos de los errores tpicos a la hora de manejar punteros [13].
1. No inicializar punteros. En el listado que se muestra a continuacin, p contiene una
direccin desconocida debido a que nunca fue definida. En otras palabras, no es posible
conocer dnde se ha escrito el valor contenido en edad.
Listado 3.3: Primer error tpico. No inicializar punteros.
1
2
3
4
5
6
7
8
int main () {
int edad, *p;
edad = 23;
*p = edad; // p?
return 0;
}
[72]
tintos compiladores tratarn estos aspectos del mismo modo. No obstante, si dos punteros
apuntan a miembros del mismo arreglo, entonces es posible compararlos. El siguiente
fragmento de cdigo muestra un ejemplo de uso incorrecto de punteros en relacin a este
tipo de errores.
No olvide que una de las claves para garantizar un uso seguro de los punteros consiste
en conocer en todo momento hacia dnde estn apuntando.
int main () {
int s[10];
int t[10];
int *p, *q;
p = s;
q = t;
if (p < q) {
// ...
}
return 0;
}
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int main () {
char s[100];
char *p;
p = s;
do {
cout << "Introduzca una cadena... ";
fgets(p, 100, stdin);
while (*p)
cout << *p++ << " ";
cout << endl;
}while (strcmp(s, "fin"));
return 0;
}
[73]
Finalmente, una estructura es un tipo agregado de datos que se puede definir como
una coleccin de variables que mantienen algn tipo de relacin lgica. Obviamente, antes
de poder crear objetos de una determinada estructura, sta ha de definirse.
El listado de cdigo siguiente muestra un ejemplo de declaracin de estructura y de su
posterior manipulacin mediante el uso de punteros. Recuerde que el acceso a los campos
de una estructura se realiza mediante el operador flecha -> en caso de acceder a ellos
mediante un puntero, mientras que el operador punto . se utiliza cuando se accede de
manera directa.
Ntese el uso del modificador const en el sePunteros y const
gundo parmetro de la funcin modificar_nombre
El uso de punteros y const puede dar
que, en este caso, se utiliza para informar al comlugar a confusin. Recuerde que const
pilador de que dicha variable no se modificar insiempre se refiere a lo que se encuentra
inmediatamente a la derecha. As, int*
ternamente en dicha funcin. Asimismo, en dicha
const p es un puntero constante, pero
funcin se hace uso del operador & en relacin a la
no as los datos a los que apunta.
variable nuevo_nombre. En la siguiente subseccin
se define y explica un nuevo concepto que C++ proporciona para la especificacin de
parmetros y los valores de retorno: las referencias.
Listado 3.6: Definicin y uso de estructuras.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;
struct persona {
string nombre;
int edad;
};
void modificar_nombre (persona *p, const string& nuevo_nombre);
int main () {
persona p;
persona *q;
p.nombre = "Luis";
p.edad = 23;
q = &p;
cout << q->nombre << endl;
modificar_nombre(q, "Sergio");
cout << q->nombre << endl;
return 0;
}
void modificar_nombre (persona *p, const string& nuevo_nombre) {
p->nombre = nuevo_nombre;
}
[74]
Por defecto, C++ utiliza el paso por valor. Sin embargo, el paso por referencia se puede realizar utilizando punteros, pasando la direccin de memoria del argumento externo
a la funcin para que sta lo modifique (si as est diseada). Este enfoque implica hacer
un uso explcito de los operadores asociados a los punteros, lo cual implica que el programador ha de pasar las direcciones de los argumentos a la hora de llamar a la funcin.
Sin embargo, en C++ tambin es posible indicar de manera automtica al compilador que
haga uso del paso por referencia: mediante el uso de referencias.
Una referencia es simplemente un nombre alternativo para un objeto. Este concepto tan sumamente simple es en realidad extremadamente til
para gestionar la complejidad. Cuando se utiliza
una referencia, la direccin del argumento se pasa
automticamente a la funcin de manera que, dentro de la funcin, la referencia se desreferencia automticamente, sin necesidad de utilizar punteros. Las referencias se declaran precediendo
el operador & al nombre del parmetro.
Paso de parmetros
Es muy importante distinguir correctamente entre paso de parmetros por
valor y por referencia para obtener el
comportamiento deseado en un programa y para garantizar que la eficiencia
del mismo no se vea penalizada.
Las referencias son muy parecidas a los punteros. Se refieren a un objeto de manera
que las operaciones afectan al objeto al cual apunta la referencia. La creacin de
referencias, al igual que la creacin de punteros, es una operacin muy eficiente.
#include <iostream>
3
4
5
6
int main () {
int x = 7, y = 13;
7
8
cout << "[" << x << ", " << y << "]" << endl; // Imprime [7, 13].
9
10
swap(x, y);
11
cout << "[" << x << ", " << y << "]" << endl; // Imprime [13, 7].
12
return 0;
13
14
15
16
17
18
19
20
a = b;
21
22
// Asigna el valor de b a a.
[75]
[76]
#include <iostream>
3
4
5
6
7
8
int main () {
double nuevo_valor;
9
10
11
12
13
14
15
f() = 7.5;
16
17
return 0;
18
19
20
21
Las ventajas de las referencias sobre los punteros se pueden resumir en que utilizar referencias es una forma de ms alto nivel de manipular objetos, ya que permite al
desarrollador olvidarse de los detalles de gestin de memoria y centrarse en la lgica del
problema a resolver. Aunque pueden darse situaciones en las que los punteros son ms
adecuados, una buena regla consiste en utilizar referencias siempre que sea posible, ya
que su sintaxis es ms limpia que la de los punteros y su uso es menos proclive a errores.
Siempre que sea posible, es conveniente utilizar referencias, debido a su sencillez,
manejabilidad y a la ocultacin de ciertos aspectos como la gestin de memoria.
3.2. Clases
3.2.
[77]
Clases
class Figura
public:
~Figura ();
6
7
10
11
12
protected:
double _x, _y;
13
};
Note cmo las variables de clase se definen como protegidas, es decir, con una visibilidad privada fuera de dicha clase a excepcin de las clases que hereden de Figura, tal y
como se discutir en la seccin 3.3.1. El constructor y el destructor comparten el nombre
con la clase, pero el destructor tiene delante el smbolo ~.
El resto de funciones sirven para modificar y
Paso por referencia
acceder al estado de los objetos instanciados a parRecuerde utilizar parmetros por refetir de dicha clase. Note el uso del modificador const
rencia const para minimizar el nmero
de copias de los mismos. El rendimienen las funciones de acceso getX() y getY() para into mejorar considerablemente.
formar de manera explcita al compilador de que
dichas funciones no modifican el estado de los objetos. A continuacin, se muestra la implementacin de las funciones definidas en la clase
Figura.
[78]
#include "Figura.h"
2
3
Figura::Figura
(double i, double j)
{
_x = i;
_y = j;
6
7
8
9
10
Figura::~Figura ()
11
12
13
14
void
15
Figura::setDim
16
(double i, double j)
17
{
_x = i;
_y = j;
18
19
20
21
22
double
23
Figura::getX () const
24
{
return _x;
25
26
27
28
double
29
Figura::getY () const
30
{
return _y;
31
32
3.2. Clases
[79]
Al igual que ocurre con otros tipos de datos, los objetos tambin se pueden manipular
mediante punteros. Simplemente se ha de utilizar la misma notacin y recordar que la
aritmtica de punteros tambin se puede usar con objetos que, por ejemplo, formen parte
de un array.
Una buena regla para usar de manera adecuada el modificador inline consiste en
evitar su uso hasta prcticamente completar el desarrollo de un proyecto. A continuacin, se puede utilizar alguna herramienta de profiling para detectar si alguna
funcin sencilla est entre las ms utilidas. Este tipo de funciones son candidatas
potenciales para modificarlas con inline y, en consecuencia, elementos para mejorar
el rendimiento del programa.
Para los objetos creados en memoria dinmica, el operador new invoca al constructor
de la clase de manera que dichos objetos existen hasta que explcitamente se eliminen con
el operador delete sobre los punteros asociados. A continuacin se muestra un listado de
cdigo que hace uso de la clase Figura previamente introducida.
Listado 3.12: Manipulacin de objetos con punteros
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "Figura.h"
using namespace std;
int main () {
Figura *f1;
f1 = new Figura (1.0, 0.5);
cout << "[" << f1->getX() << ", " << f1->getY() << "]" << endl;
delete f1;
return 0;
}
[80]
Un uso bastante comn de las funciones amigas se da cuando existen dos o ms clases
con miembros que de algn modo estn relacionados. Por ejemplo, imagine dos clases
distintas que hacen uso de un recurso comn cuando se da algn tipo de evento externo.
Por otra parte, otro elemento del programa necesita conocer si se ha hecho uso de dicho
recurso antes de poder utilizarlo para evitar algn tipo de inconsistencia futura. En este
contexto, es posible crear una funcin en cada una de las dos clases que compruebe,
consultado una variable booleana, si dicho recurso fue utilizado, provocando dos llamadas
independientes. Si esta situacin se da continuamente, entonces se puede llegar a producir
una sobrecarga de llamadas.
Por el contrario, el uso de una funcin amiga permitira comprobar de manera directa
el estado de cada objeto mediante una nica llamada que tenga acceso a las dos clases.
En este tipo de situaciones, las funciones amigas contribuyen a un cdigo ms limpio y
mantenible. El siguiente listado de cdigo muestra un ejemplo de este tipo de situaciones.
3
4
class B;
5
6
7
class A {
int _estado;
8
9
10
public:
A() {_estado = NO_USADO;}
void setEstado (int estado) {_estado = estado;}
11
12
13
};
14
15
16
class B {
int _estado;
17
18
19
public:
B() {_estado = NO_USADO;}
void setEstado (int estado) {_estado = estado;}
20
21
22
};
23
25
int usado (A a, B b) {
return (a._estado || b._estado);
26
24
En el ejemplo, lnea 21 , se puede apreciar cmo la funcin recibe dos objetos como
parmetros. Al contrario de lo que ocurre en otros lenguajes como Java, en C++ los objetos, por defecto, se pasan por valor. Esto implica que en realidad la funcin recibe una
copia del objeto, en lugar del propio objeto con el que se realiz la llamada inicial. Por
tanto, los cambios realizados dentro de la funcin no afectan a los argumentos. Aunque el
paso de objetos es un procedimiento sencillo, en realidad se generan ciertos eventos que
pueden sorprender inicialmente. El siguiente listado de cdigo muestra un ejemplo.
3.2. Clases
[81]
#include <iostream>
3
5
class A {
int _valor;
public:
8
}
~A() {
10
11
}
12
13
14
15
16
17
};
18
19
void mostrar (A a) {
cout << a.getValor() << endl;
20
21
22
23
int main () {
24
A a(7);
25
mostrar(a);
return 0;
26
27
[82]
En el caso de devolver objetos al finalizar la ejecucin de una funcin se puede producir un problema similar, ya que el objeto temporal que se crea para almacenar el valor
de retorno tambin realiza una llamada a su destructor. La solucin pasa por devolver un
puntero o una referencia, pero en casos en los que no sea posible el constructor de copia
puede contribuir a solventar este tipo de problemas.
Construyendo...
Constructor copia...
7
Destruyendo...
Destruyendo...
3.2. Clases
[83]
#include <iostream>
3
5
class A {
int *_valor;
public:
A(int valor);
// Constructor.
~A();
// Destructor.
10
int getValor () const { return *_valor;}
11
12
};
13
14
15
16
*_valor = valor;
17
18
19
20
21
22
23
24
25
26
A::~A () {
cout << "Destruyendo..." << endl;
delete _valor;
27
28
29
30
31
void mostrar (A a) {
cout << a.getValor() << endl;
32
33
34
35
int main () {
36
A a(7);
37
mostrar(a);
return 0;
38
39
[84]
class Point3D {
public:
Point3D ():
_x(0), _y(0), _z(0) {}
3
4
6
8
10
private:
int _x, _y, _z;
11
};
12
13
Point3D
14
Point3D::operator+
15
16
Point3D resultado;
17
resultado._x = this->_x + op2._x;
resultado._y = this->_y + op2._y;
resultado._z = this->_z + op2._z;
18
19
20
21
return resultado;
22
23
En C++, tambin es posible sobrecargar operadores unarios, como por ejemplo ++.
En este caso particular, no sera necesario pasar ningn parmetro de manera explcita, ya
que la operacin afectara al parmetro implcito this.
Otro uso importante de la sobrecarga de operadores est relacionado con los problemas discutidos en la seccin 3.2.2. C++ utiliza un constructor de copia por defecto que
se basa en realizar una copia exacta del objeto cuando ste se pasa como parmetro a una
funcin, cuando se devuelve de la misma o cuando se inicializa. Si el constructor de una
clase realiza una reserva de recursos, entonces el uso implcito del constructor de copia
por defecto puede generar problemas. La solucin, como se ha comentado anteriormente,
es el constructor de copia.
3.2. Clases
[85]
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class A {
char *_valor;
public:
A() {_valor = 0;}
A(const A &obj); // Constructor de copia.
~A() {if(_valor) delete [] _valor;
cout << "Liberando..." << endl;}
void mostrar () const {cout << _valor << endl;}
void set (char *valor);
};
A::A(const A &obj) {
_valor = new char[strlen(obj._valor) + 1];
strcpy(_valor, obj._valor);
}
void A::set (char *valor) {
delete [] _valor;
_valor = new char[strlen(valor) + 1];
strcpy(_valor, valor);
}
El constructor de copia se define entre las lneas 18 y 21 , reservando una nueva regin
de memoria para el contenido
de _valor y copiando el mismo en la variable miembro. Por
otra parte, las lneas 23-26 muestran la implementacin de la funcin set, que modifica el
contenido de dicha variable miembro.
El siguiente listado muestra la implementacin de la funcin entrada, que pide una
cadena por teclado y devuelve un objeto que alberga la entrada proporcionada por el
usuario.
En primer lugar, el programa se comporta adecuadamente cuando se llama a entrada,
particularmente cuando se devuelve la copia del objeto a, utilizando el constructor de
copia previamente definido. Sin embargo, el programar abortar abruptamente cuando el
objeto devuelto por entrada se asigna a obj en la funcin principal. Recuerde que en este
caso se efectua una copia idntica. El problema reside en que obj.valor apunta a la misma
direccin de memoria que el objeto temporal, y ste ltimo se destruye despus de volver
desde entrada, por lo que obj.valor apunta a memoria que acaba de ser liberada. Adems,
obj.valor se vuelve a liberar al finalizar el programa.
[86]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
A entrada () {
char entrada[80];
A a;
cout << "Introduzca texto... ";
cin >> entrada;
a.set(entrada);
return a;
}
int main () {
A obj;
obj = entrada(); // Fallo.
obj.mostrar();
return 0;
}
3.3.
Herencia y polimorfismo
[87]
3.3.1. Herencia
El siguiente listado de cdigo muestra la clase base Vehculo que, desde un punto
de vista general, define un medio de transporte por carretera. De hecho, sus variables
miembro son el nmero de ruedas y el nmero de pasajeros.
Listado 3.20: Clase base Vehculo
1
2
3
4
5
6
7
8
9
10
class Vehiculo {
int _ruedas;
// Privado. No accesible en clases derivadas.
int _pasajeros;
public:
void setRuedas (int ruedas) {_ruedas = ruedas;}
int getRuedas () const {return _ruedas;}
void setPasajeros (int pasajeros) {_pasajeros = pasajeros;}
int getPasajeros () const {return _pasajeros;}
};
La clase base anterior se puede extender para definir coches con una nueva caracterstica propia de los mismos, como se puede apreciar en el siguiente listado.
#include <iostream>
#include "Vehiculo.cpp"
4
5
6
7
8
9
10
public:
void setPMA (int PMA) {_PMA = PMA;}
int getPMA () const {return _PMA;}
11
12
13
14
15
16
17
}
};
[88]
En este ejemplo no se han definido los constructores de manera intencionada para discutir el
acceso a los miembros
de la clase. Como se puede
apreciar en la lnea 5 del siguiente listado, la clase Coche hereda de la clase Vehculo, utilizando el
operador :. La palabra reservada public delante de
Vehculo determina el tipo de acceso. En este caso concreto, el uso de public implica que todos los
miembros pblicos de la clase base sern tambin
miembros pblicos de la clase derivada. En otras palabras, el efecto que se produce equivale a que los miembros pblicos de Vehculo se hubieran declarado dentro de Coche. Sin
embargo, desde Coche no es posible acceder a los miembros privados de Vehculo, como
por ejemplo a la variable _ruedas.
El caso contrario a la herencia pblica es la herencia privada. En este caso, cuando la
clase base se hereda con private, entonces todos los miembros pblicos de la clase base
se convierten en privados en la clase derivada.
Adems de ser pblico o privado, un miembro de clase se puede definir como protegido. Del mismo modo, una clase base se puede heredar como protegida. Si un miembro
se declara como protegido, dicho miembro no es accesible por elementos que no sean
miembros de la clase salvo en una excepcin. Dicha excepcin consiste en heredar un
miembro protegido, hecho que marca la diferencia entre private y protected. En esencia,
los miembros protegidos de la clase base se convierten en miembros protegidos de la clase
derivada. Desde otro punto de vista, los miembros protegidos son miembros privados de
una clase base pero con la posibilidad de heredarlos y acceder a ellos por parte de una
clase derivada. El siguiente listado de cdigo muestra el uso de protected.
Herencia y acceso
El modificador de acceso cuando se usa
herencia es opcional. Sin embargo, si
ste se especifica ha de ser public, protected o private. Por defecto, su valor
es private si la clase derivada es efectivamente una clase. Si la clase derivada
es una estructura, entonces su valor por
defecto es public.
#include <iostream>
using namespace std;
class Vehiculo {
protected:
int _ruedas;
// Accesibles en Coche.
int _pasajeros;
// ...
};
class Coche : protected Vehiculo {
int _PMA;
public:
// ...
void mostrar () const {
cout << "Ruedas: " << _ruedas << endl;
cout << "Pasajeros: " << _pasajeros << endl;
cout << "PMA: " << _PMA << endl;
}
};
Otro caso particular que resulta relevante comentar se da cuando una clase base se
hereda como privada. En este caso, los miembros protegidos se heredan como miembros
privados en la clase protegida.
Si una clase base se hereda como protegida mediante el modificador de acceso protected, entonces todos los miembros pblicos y protegidos de dicha clase se heredan como
miembros protegidos en la clase derivada.
[89]
Constructores y destructores
Cuando se hace uso de herencia y se definen constructores y/o destructores de clase,
es importante conocer el orden en el que se ejecutan en el caso de la clase base y de
la clase derivada, respectivamente. Bsicamente, a la hora de construir un objeto de una
clase derivada, primero se ejecuta el constructor de la clase base y, a continuacin, el
constructor de la derivada. En el caso de la destruccin de objetos, el orden se invierte, es
decir, primero se ejecuta el destructor de la clase derivada y, a continuacin, el de la clase
base.
Otro aspecto relevante est vinculado al paso de
parmetros al constructor de la clase base desde el
constructor de la clase derivada. Para ello, simplemente se realiza una llamada al constructor de la
clase base, pasando los argumentos que sean necesarios. Este planteamiento es similar al utilizado en
Java mediante super(). Note que aunque una clase derivada no tenga variables miembro,
en su constructor han de especificarse aquellos parmetros que se deseen utilizar para
llamar al constructor de la clase base.
Inicializacin de objetos
El constructor de una clase debera
inicializar idealmente todo el estado
de los objetos instanciados. Utilice el
constructor de la clase base cuando as
sea necesario.
Recuerde que, al utilizar herencia, los constructores se ejecutan en el orden de su derivacin mientras que los destructores se ejecutan en el orden inverso a la derivacin.
[90]
Otro problema concreto con este enfoque es la duplicidad de cdigo, ya que la clase
ObjetoJuego puede no ser la nica en recibir mensajes, por ejemplo. La clase Jugador
podra necesitar recibir mensajes sin ser un tipo particular de la primera clase. En el caso
de enlazar con una estructura de rbol se podra dar el mismo problema, ya que otros
elementos del juego, como por ejemplo los nodos de una escena se podra organizar del
mismo modo y haciendo uso del mismo tipo de estructura de rbol. En este contexto,
copiar el cdigo all donde sea necesario no es una solucin viable debido a que complica
enormemente el mantenimiento y afecta de manera directa a la arquitectura del diseo.
Enfoque basado en agregacin
La conclusin directa que se obtiene al reflexionar sobre el anterior enfoque es que resulta necesario disear sendas clases, ReceptorMensajes y NodoArbol, para representar la
funcionalidad previamente discutida. La cuestin reside en cmo relacionar dichas clases
con la clase ObjetoJuego.
Una opcin inmediata podra ser la agregacin,
de manera que un objeto de la clase ObjetoJuego
contuviera un objeto de la clase ReceptorMensajes
y otro de la clase NodoArbol, respectivamente. As,
la clase ObjetoJuego sera responsable de proporcionar la funcionalidad necesaria para manejarlos
en su propia interfaz. En trminos generales, esta solucin proporciona un gran nivel de
reutilizacin sin incrementar de manera significativa la complejidad de las clases que se
extienden de esta forma.
El siguiente listado de cdigo muestra una posible implementacin de este diseo.
Contenedores
La aplicacin de un esquema basado en
agregacin, de manera que una clase
contiene elementos relevantes vinculados a su funcionalidad, es en general un
buen diseo.
class ObjetoJuego {
public:
bool recibirMensaje (const Mensaje &m);
3
4
6
// ...
8
9
private:
ReceptorMensajes *_receptorMensajes;
NodoArbol *_nodoArbol;
10
11
12
};
13
15
16
14
17
19
20
18
21
23
24
22
Objeto
Juego
[91]
Receptor
Mensajes
Receptor
Mensajes
Nodo
rbol
Nodo
rbol
Receptor
Mensajes
Nodo
rbol
Objeto
Juego
Objeto
Juego
(a)
(b)
(c)
Figura 3.2: Distintas soluciones de diseo para el problema de la clase ObjetoJuego. (a) Uso de agregacin. (b)
Herencia simple. (c) Herencia mltiple.
[92]
Desde un punto de vista general, la herencia mltiple puede introducir una serie de
complicaciones y desventajas, entre las que destacan las siguientes:
Ambigedad, debido a que las clases base de las que hereda una clase derivada
pueden mantener el mismo nombre para una funcin. Para solucionar este problema, se puede explicitar el nombre de la clase base antes de hacer uso de la funcin,
es decir, ClaseBase::Funcion.
Topografa, debido a que se puede dar la situacin en la que una clase derivada
herede de dos clases base, que a su vez heredan de otra clase, compartiendo todas
ellas la misma clase. Este tipo de rboles de herencia puede generar consecuencias
inesperadas, como duplicidad de variables y ambigedad. Este tipo de problemas
se puede solventar mediante herencia virtual, concepto distinto al que se estudiar
en el siguiente apartado relativo al uso de funciones virtuales.
Arquitectura del programa, debido a que el uso de la herencia, simple o mltiple,
puede contribuir a degradar el diseo del programa y crear un fuerte acoplamiento
entre las distintas clases que la componen. En general, es recomendable utilizar alternativas como la composicin y relegar el uso de la herencia mltiple slo cuando
sea la mejor alternativa real.
[93]
#include "Coche.cpp"
2
3
int main () {
Vehiculo *v; // Puntero a objeto de tipo vehculo.
Coche c;
// Objeto de tipo coche.
4
5
6
7
c.setRuedas(4);
c.setPasajeros(7);
c.setPMA(1885);
// Se establece el estado de c.
10
11
v = &c;
12
13
return 0;
14
15
Cuando se utilizan punteros a la clase base, es importante recordar que slo es posible
acceder a aquellos elementos que pertenecen a la clase base. En el listado de cdigo 3.26
se ejemplifica cmo no es posible acceder a elementos de una clase derivada utilizando
un puntero a la clase base.
[94]
#include "Coche.cpp"
2
3
int main () {
Vehiculo *v; // Puntero a objeto de tipo vehculo.
Coche c;
// Objeto de tipo coche.
4
5
6
7
c.setRuedas(4);
c.setPasajeros(7);
c.setPMA(1885);
// Se establece el estado de c.
10
v = &c;
11
12
cout << v->getPMA() << endl; // ERROR en tiempo de compilacin.
13
14
15
16
17
18
return 0;
19
20
Otro punto a destacar est relacionado con la aritmtica de punteros. En esencia, los
punteros se incrementan o decrementan de acuerdo a la clase base. En otras palabras,
cuando un puntero a una clase base est apuntando a una clase derivada y dicho puntero se
incrementa, entonces no apuntar al siguiente objeto de la clase derivada. Por el contrario,
apuntar a lo que l cree que es el siguiente objeto de la clase base. Por lo tanto, no es
correcto incrementar un puntero de clase base que apunta a un objeto de clase derivada.
Finalmente, es importante destacar que, al igual que ocurre con los punteros, una
referencia a una clase base se puede utilizar para referenciar a un objeto de la clase
derivada. La aplicacin directa de este planteamiento se da en los parmetros de una
funcin.
Uso de funciones virtuales
Una funcin virtual es una funcin declarada
como virtual en la clase base y redefinida en una o
ms clases derivadas. De este modo, cada clase derivada puede tener su propia versin de dicha funcin. El aspecto interesante es lo que ocurre cuando se llama a esta funcin con un puntero
o referencia a la clase base. En este contexto, C++ determina en tiempo de ejecucin qu
versin de la funcin se ha de ejecutar en funcin del tipo de objeto al que apunta el
puntero.
La palabra clave virtual
Una clase que incluya una funcin virtual es una clase polimrfica.
[95]
#include <iostream>
using namespace std;
class Base {
public:
virtual void imprimir () const { cout << "Soy Base!" << endl; }
};
class Derivada1 : public Base {
public:
void imprimir () const { cout << "Soy Derivada1!" << endl; }
};
class Derivada2 : public Base {
public:
void imprimir () const { cout << "Soy Derivada2!" << endl; }
};
int main ()
Base *pb,
Derivada1
Derivada2
{
base_obj;
d1_obj;
d2_obj;
pb = &base_obj;
pb->imprimir(); // Acceso a imprimir de Base.
pb = &d1_obj;
pb->imprimir(); // Acceso a imprimir de Derivada1.
pb = &d2_obj;
pb->imprimir(); // Acceso a imprimir de Derivada2.
return 0;
}
Las funciones virtuales han de ser miembros de la clase en la que se definen, es decir,
no pueden ser funciones amigas. Sin embargo, una funcin virtual puede ser amiga de
otra clase. Adems, los destructores se pueden definir como funciones virtuales, mientras
que en los constructores no.
Las funciones virtuales se heredan de manera
Sobrecarga/sobreescrit.
independiente del nmero de niveles que tenga la
Cuando una funcin virtual se redefijerarqua de clases. Suponga que en el ejemplo anne en una clase derivada, la funcin se
sobreescribe. Para sobrecargar una funterior Derivada2 hereda de Derivada1 en lugar de
cin, recuerde que el nmero de parheredar de Base. En este caso, la funcin imprimir
metros y/o sus tipos han de ser diferenseguira siendo virtual y C++ sera capaz de selectes.
cionar la versin adecuada al llamar a dicha funcin. Si una clase derivada no sobreescribe una funcin virtual definida en la clase base,
entonces se utiliza la versin de la clase base.
El polimorfismo permite manejar la complejidad de los programas, garantizando la
escalabilidad de los mismos, debido a que se basa en el principio de una interfaz, mltiples mtodos. Por ejemplo, si un programa est bien diseado, entonces se puede suponer
que todos los objetos que derivan de una clase base se acceden de la misma forma, incluso
si las acciones especficas varan de una clase derivada a la siguiente. Esto implica que
slo es necesario recordar una interfaz. Sin embargo, la clase derivada es libre de aadir
uno o todos los aspectos funcionales especificados en la clase base.
[96]
#include <iostream>
using namespace std;
class Figura { // Clase abstracta Figura.
public:
virtual float area () const = 0; // Funcin virtual pura.
};
class Circulo : public Figura {
public:
Circulo (float r): _radio(r) {}
void setRadio (float r) { _radio = r; }
float getRadio () const { return _radio; }
// Redefinicin de area () en Crculo.
float area () const { return _radio * _radio * 3.14; }
private:
float _radio;
};
int main () {
Figura *f;
Circulo c(1.0);
f = &c;
cout << "AREA: " << f->area() << endl;
// Recuerde realizar un casting al acceder a func. especfica.
cout << "Radio:" << static_cast<Circulo*>(f)->getRadio() << endl;
return 0;
}
3.4. Plantillas
[97]
Recuerde que una clase con una o ms funciones virtuales puras es una clase abstracta y, por lo
tanto, no se pueden realizar instancias a partir de
ella. En realidad, la clase abstracta define una interfaz que sirve como contrato funcional para el
resto de clases que hereden a partir de la misma.
En el ejemplo anterior, la clase Circulo est obligada a implementar la funcin area en
caso de definirla. En caso contrario, el compilador generar un error. De hecho, si una
funcin virtual pura no se define en una clase derivada, entonces dicha funcin virtual
sigue siendo pura y, por lo tanto, la clase derivada es tambin una clase abstracta.
El uso ms relevante de las clases abstractas consiste en proporcionar una interfaz
sin revelar ningn aspecto de la implementacin subyacente. Esta idea est fuertemente
relacionada con la encapsulacin, otro de los conceptos fundamentales de la POO junto
con la herencia y el polimorfismo.
Fundamentos POO
La encapsulacin, la herencia y el polimorfimos representan los pilares fundamentales de la programacin orientada a objetos.
3.4.
Plantillas
General y ptimo
Recuerde que en el desarrollo de videojuegos siempre existe un compromiso
entre plantear una solucin general y
una solucin optimizada para la plataforma final.
[98]
1
2
3
4
5
6
7
8
9
10
11
class Entidad {
public:
// Funcionalidad de la lista.
Entidad * getSiguiente ();
void eliminar ();
void insertar (Entidad *pNuevo);
private:
// Puntero a la cabeza de la lista.
Entidad *_pSiguiente;
};
Otra posible solucin consiste en hacer uso de la herencia para definir una clase base que represente a cualquier elemento de una lista (ver figura 3.3.b). De este modo,
cualquier clase que desee incluir la funcionalidad asociada a la lista simplemente ha de
extenderla. Este planteamiento permite tratar a los elementos de la lista mediante polimorfismo. Sin embargo, la mayor desventaja que presenta este enfoque est en el diseo,
ya que no es posible separar la funcionalidad de la lista de la clase propiamente dicha, de
manera que no es posible tener el objeto en mltiples listas o en alguna otra estructura de
datos. Por lo tanto, es importante separar la propia lista de los elementos que realmente
contiene.
Una alternativa para proporcionar esta separacin consiste en hacer uso de una lista
que maneja punteros de tipo nulo para albergar distintos tipos de datos (ver figura 3.3.c).
De este modo, y mediante los moldes correspondientes, es posible tener una lista con
elementos de distinto tipo y, al mismo tiempo, la funcionalidad de la misma est separada
del contenido. La principal desventaja de esta aproximacin es que no es type-safe, es
decir, depende del programador incluir la funcionalidad necesaria para convertir tipos, ya
que el compilador no los detectar.
Otra desventaja de esta propuesta es que son necesarias dos reservas de memoria para
cada uno de los nodos de la lista: una para el objeto y otra para el siguiente nodo de
la lista. Este tipo de cuestiones han de considerarse de manera especial en el desarrollo
de videojuegos, ya que la plataforma hardware final puede tener ciertas restricciones de
recursos.
La figura 3.3 muestra de manera grfica las distintas opciones discutidas hasta ahora en lo relativo a la implementacin de una lista que permita el tratamiento de datos
genricos.
3.4. Plantillas
<MiClase>
[99]
<MiClase>
<MiClase>
<MiClase>
(a)
Elemento
Lista
MiClase
Elemento
Lista
Elemento
Lista
<MiClase>
<MiClase>
(b)
(c)
Figura 3.3: Distintos enfoques para la implementacin de una lista con elementos gnericos. (a) Integracin en
la propia clase de dominio. (b) Uso de herencia. (c) Contenedor con elementos de tipo nulo.
Listado 3.30: Implementacin de un tringulo con plantilla
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
template<class T> // Tipo general T.
class Triangle {
public:
Triangle (const T &v1, const T &v2, const T &v3):
_v1(v1), _v2(v2), _v3(v3) {}
~Triangle () {}
T getV1 () const { return _v1; }
T getV2 () const { return _v2; }
T getV3 () const { return _v3; }
private:
T _v1, _v2, _v3;
};
class Vec2 {
public:
Vec2 (int x, int y): _x(x), _y(y) {}
~Vec2 () {}
int getX () const { return _x; }
int getY () const { return _y; }
private:
int _x, _y;
};
int main () {
Vec2 p1(2, 7), p2(3, 4), p3(7, 10);
Triangle<Vec2> t(p1, p2, p3); // Instancia de la plantilla.
cout << "V1: [" << t.getV1().getX() << ", "
<< t.getV1().getY() << "]" << endl;
return 0;
}
[100]
Como se puede apreciar, se ha definido una clase Tringulo que se puede utilizar
para almacenar cualquier tipo de datos. En este ejemplo, se ha instanciado un tringulo
con elementos del tipo Vec2, que representan valores en el espacio bidimensional. Para
ello, se ha utilizado la palabra clave template para completar la definicin de la clase
Tringulo, permitiendo el manejo de tipos genricos T. Note cmo este tipo genrico se
usa para declarar las variables miembro de dicha clase.
Clase Vec2
La clase Vec2 se podra haber extendido mediante el uso de plantillas para manejar otros tipos de datos comunes a la representacin de puntos en el
espacio bidimensional, como por ejemplo valores en punto flotante.
#include <iostream>
using namespace std;
template<class T> // Tipo general T.
void swap (T &a, T &b) {
T aux(a);
a = b;
b = aux;
}
int main () {
string a = "Hello", b = "Good-bye";
cout << "[" << a << ", " << b << "]" << endl;
swap(a, b); // Se instancia para cadenas.
cout << "[" << a << ", " << b << "]" << endl;
return 0;
}
Dicha funcin puede utilizarse con enteros, valores en punto flotante, cadenas o cualquier clase con un constructor de copia y un constructor de asignacin. Adems, la funcin
se instancia dependiendo del tipo de datos utilizado. Recuerde que no es posible utilizar
dos tipos de datos distintos, es decir, por ejemplo un entero y un valor en punto flotante,
ya que se producir un error en tiempo de compilacin.
El uso de plantillas en C++ solventa todas las necesidades planteadas para manejar las
listas introducidas en la seccin 3.4.1, principalmente las siguientes:
Flexibilidad, para poder utilizar las listas con distintos tipos de datos.
Simplicidad, para evitar la copia de cdigo cada vez que se utilice una estructura
de lista.
Uniformidad, ya que se maneja una nica interfaz para la lista.
Independencia, entre el cdigo asociado a la funcionalidad de la lista y el cdigo
asociado al tipo de datos que contendr la lista.
A continuacin se muestra la implementacin de una posible solucin, la cual est
compuesta por dos clases generales. La primera de ellas se utilizar para los nodos de la
lista y la segunda para especificar la funcionalidad de la propia lista.
3.4. Plantillas
Uso de plantillas
Las plantillas son una herramienta excelente para escribir cdigo que no dependa de un tipo de datos especfico.
[101]
Como se puede apreciar en el siguiente listado
de cdigo, las dos clases estn definidas para poder
utilizar cualquier tipo de dato y, adems, la funcionalidad de la lista es totalmente independiente del
su contenido.
template<class T>
class NodoLista {
public:
NodoLista (T datos);
T & getDatos ();
NodoLista * siguiente ();
private:
T _datos;
};
template<class T>
class Lista {
public:
NodoLista<T> getCabeza ();
void insertarFinal (T datos);
// Resto funcionalidad...
private:
NodoLista<T> *_cabeza;
};
[102]
Desde una perspectiva general, no debe olvidar que las plantillas representan una herramienta adecuada para un determinado uso, por lo que su uso indiscriminado es un error.
Recuerde tambin que las plantillas introducen una dependencia de uso respecto a otras
clases y, por lo tanto, su diseo debera ser simple y mantenible.
Una de las situaciones en las que el uso de plantillas resulta adecuado est asociada al uso de contenedores, es decir, estructuras de datos que contienen objetos de distintas clases. En este contexto, es
importante destacar que la biblioteca STL de C++
ya proporciona una implementacin de listas, adems de otras estructuras de datos y de algoritmos
listos para utilizarse. Por lo tanto, es bastante probable que el desarrollador haga un uso
directo de las mismas en lugar de tener que desarrollar desde cero su propia implementacin. En el captulo 5 se estudia el uso de la biblioteca STL y se discute su uso en el
mbito del desarrollo de videojuegos.
Equipo de desarrollo
Es importante recordar la experiencia
de los compaeros, actuales y futuros,
en un grupo de desarrollo a la hora
de introducir dependencias con aspectos avanzados en el uso de plantillas en
C++.
3.5.
Manejo de excepciones
[103]
[104]
nmica.
#include <iostream>
#include <exception>
using namespace std;
int main () {
try {
int *array = new int[1000000];
}
catch (bad_alloc &e) {
cerr << "Error al reservar memoria." << endl;
}
return 0;
}
Como se ha comentado anteriormente, la sentencia throw se utiliza para arrojar excepciones. C++ es estremadamente flexible y permite lanzar un objeto de cualquier tipo
de datos como excepcin. Este planteamiento posibilita la creacin de excepciones definidas por el usuario que modelen las distintas situaciones de error que se pueden dar en
un programa e incluyan la informacin ms relevante vinculadas a las mismas.
El siguiente listado de cdigo muestra un ejemplo de creacin y tratamiento de excepciones definidas por el usuario.
En particular, el cdigo define una excepcin general en las lneas 4-10 mediante la
definicin de la clase MiExcepcion, que tiene como variable miembro una cadena de texto
que se utilizar para indicar la razn de la excepcin.
En la funcin main, se lanza una
Normalmente, ser necesario tratar con distintos tipos de excepciones en un mismo programa.
Un enfoque bastante comn consiste en hacer uso
de una jerarqua de excepciones, con el mismo planteamiento usado en una jerarqua de clases, para
modelar distintos tipos de excepciones especficas.
[105]
#include <iostream>
using namespace std;
class MiExcepcion {
const string &_razon;
public:
MiExcepcion (const string &razon): _razon(razon) {}
const string &getRazon () const {return _razon;}
};
int main () {
int valor;
const string &r = "Valor introducido incorrecto.";
try {
cout << "Introduzca valor entre 1 y 10...";
cin >> valor;
if ((valor < 1) || (valor > 10)) {
throw MiExcepcion(r);
}
}
catch (MiExcepcion &e) {
cerr << e.getRazon() << endl;
}
return 0;
}
La figura 3.4 muestra un ejemplo representativo vinculado con el desarrollo de videojuegos, en la que se plantea una jerarqua con una clase base y tres especializaciones
asociadas a errores de gestin de memoria, E/S y operaciones matemticas.
MiExcepcin
MiExcepcinMemoria
MiExcepcinIO
MiExcepcinMatemtica
[106]
Como se ha comentado anteriormente, los bloques de sentencias try-catch son realmente flexibles y posibilitan la gestin de diversos tipos de excepciones. El siguiente
listado de cdigo muestra un ejemplo en el que se carga informacin tridimensional en
una estructura de datos.
La idea del siguiente fragmento de cdigo se puede resumir en que el desarrollador
est preocupado especialmente por la gestin de errores de entrada/salida o mtematicos
(primer y segundo bloque catch, respectivamente) pero, al mismo tiempo, no desea que
otro tipo de error se propague a capas superiores, al menos un error que est definido en
la jerarqua de excepciones (tercer bloque catch). Finalmente, si se desea que el programa
capture cualquier tipo de excepcin, entonces se puede aadir una captura genrica (ver
cuarto bloque catch). En resumen, si se lanza una excepcin no contemplada en un bloque
catch, entonces el programa seguir buscando el bloque catch ms cercano.
Listado 3.35: Gestin de mltiples excepciones
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Es importante resaltar que el orden de las sentencias catch es relevante, ya que dichas sentencias siempre se procesan de arriba a abajo. Adems, cuando el programa encuentra un bloque que
trata con la excepcin lanzada, el resto de bloques
se ignoran automticamente.
Otro aspecto que permite C++ relativo al manejo de excepciones es la posibilidad de re-lanzar una excepcin, con el objetivo de delegar
en una capa superior el tratamiento de la misma. El siguiente listado de cdigo muestra
un ejemplo en el que se delega el tratamiento del error de entrada/salida.
Exception handlers
El tratamiento de excepciones se puede enfocar con un esquema parecido
al del tratamiento de eventos, es decir,
mediante un planteamiento basado en
capas y que delege las excepciones para su posterior gestin.
[107]
Smart pointers
En C++ es bastante comn utilizar herramientas que permitan manejar los
punteros de una forma ms cmodo.
Un ejemplo representativo son los punteros inteligentes o smart pointers.
[108]
3
4
try {
6
7
9
10
11
12
delete pTextura;
13
pTexture = NULL;
}
14
15
fclose(entrada);
16
return pTextura;
17
18
En el caso del puntero a la textura, es posible aplicar una solucin ms sencilla para
todos los punteros: el uso de la plantilla unique_ptr. Dicha clase forma parte del estndar
de 2011 de C++ y permite plantear un enfoque similar al del anterior manejador pero
con punteros en lugar de con ficheros. Bsicamente, cuando un objeto de dicha clase se
destruye, entonces el puntero asociado tambin se libera correctamente. A continuacin
se muestra un listado de cdigo con las dos soluciones discutidas.
Listado 3.38: Uso adecuado de excepciones (simple)
1
2
3
4
5
6
7
8
[109]
[110]
[112]
4.1.
Introduccin
El diseo de una aplicacin es un proceso iterativo y de continuo refinamiento. Normalmente, una aplicacin es lo suficientemente compleja como para que su diseo tenga
que ser realizado por etapas, de forma que al principio se identifican los mdulos ms
abstractos y, progresivamente, se concreta cada mdulo con un diseo en particular.
En el camino, es comn encontrar problemas y situaciones que conceptualmente pueden parecerse entre s, por lo menos a priori. Quizs un estudio ms exhaustivo de los
requisitos (o del contexto) permitan determinar si realmente se trata de problemas equivalentes.
Por ejemplo, supongamos que para resolver un determinado problema se llega a la
conclusin de que varios tipos de objetos deben esperar a un evento producido por otro.
Esta situacin puede darse en la creacin de una interfaz grfica donde la pulsacin de
un botn dispara la ejecucin de otras acciones. Pero tambin es similar a la implementacin de un manejador del teclado, cuyas pulsaciones son recogidas por los procesos
interesados, o la de un gestor de colisiones, que notifica choques entre elementos del juego. Incluso se parece a la forma en que muchos programas de chat envan mensajes a un
grupo de usuarios.
Ciertamente, cada uno de los ejemplos anteriores tendr una implementacin diferente
y no es posible (ni a veces deseable) aplicar exactamente la misma solucin a cada uno de
ellos. Sin embargo, s que es cierto que existe semejanza en la esencia del problema. En
nuestro ejemplo, en ambos casos existen entidades que necesitan ser notificadas cuando
ocurre un cierto evento. Esa semejanza en la esencia del problema que une los diseos de
dos soluciones tiene que verse reflejada, de alguna manera, en la implementacin final.
Los patrones de diseo son formas bien conocidas y probadas de resolver problemas
de diseo que son recurrentes en el tiempo. Los patrones de diseo son ampliamente utilizados en las disciplinas creativas y tcnicas. As, de la misma forma que un guionista de
cine crea guiones a partir de patrones argumentales como comedia o ciencia-ficcin,
un ingeniero se basa en la experiencia de otros proyectos para identificar patrones comunes que le ayuden a disear nuevos procesos. De esta forma, reutilizando soluciones bien
probadas y conocidas se ayuda a reducir el tiempo necesario para el diseo.
Segn [4], un patrn de diseo es una descripcin de la comunicacin entre objetos
y clases personalizadas para solucionar un problema genrico de diseo bajo un contexto
determinado. Los patrones sintetizan la tradicin y experiencia profesional de diseadores
de software experimentados que han evaluado y demostrado que la solucin proporcionada es una buena solucin bajo un determinado contexto. El diseador o desarrollador que
conozca diferentes patrones de diseo podr reutilizar estas soluciones, pudiendo alcanzar
un mejor diseo ms rpidamente.
4.1. Introduccin
[113]
Object
Object
Method
Field
Type
Method
Field
Method call
Field Use
State change
Cohesion
Type
Is of type
Returns of type
Is of type
Subtyping
Tabla 4.1: Interacciones entre las entidades bsicas de la POO (versin simplificada) [8]
Para los lenguajes de alto nivel que ofrecen la posibilidad de trabajar con diseo orientado a objetos cabe preguntarse cules son las estructuras bsicas con las que contamos.
Cules son las relaciones mnimas entre estas entidades que podemos usar para el crear
diseos? En general, cules son las piezas atmicas del diseo?
En [8] se da respuesta a estas preguntas. Basndose en el diseo orientado a objetos,
se definen 4 entidades bsicas fundamentales:
Tipos: normalmente clases, aunque es vlido para lenguajes que no soportan clases.
Mtodos: operaciones que se pueden realizar sobre un tipo.
Campos: que pueden ser variables o atributos.
Objetos: instancias de un tipo determinado y que tiene entidad por s mismo.
Si combinamos los 4 elementos anteriores unos con otros sistemticamente y pensamos en la relacin que pueden tener se obtiene un resultado similar al mostrado en el
cuadro 4.1. De forma simplificada y obviando algunas otras, aparecen las principales relaciones de dependencia entre las entidades. Por ejemplo, la relacin object-type es aquella
en la que una instancia depende de un tipo de determinado. Esta relacin es cuando decimos que un objeto es de tipo X. field-method se produce cuando el valor devuelto por
un mtodo podemos llamarla cambio de estado. Todas estas relaciones y conceptos muy
bsicos y seguro que son de sobra conocidas. Sin embargo, resulta interesante estudiarlas
por separado y como piezas bsicas de un rompecabezas an mayor que es el diseo de
aplicaciones.
La relacin method-method es aquella en la que un mtodo primero llama a otro como
parte de su implementacin. Esta simple relacin nos permitir definir patrones bsicos
de diseo como veremos ms adelante. En este captulo slo nos centraremos en esta
relacin pero en [8] puede encontrarse un estudio detallado de las dems relaciones y
consiguientes patrones de diseo elementales.
[114]
[115]
4.2.
Patrones elementales
En esta seccin se describen algunos (no todos) de los patrones elementales emergidos
de la relacin method-method, es decir, cuando un mtodo A llama desde su implementacin a un mtodo B. Esta relacin es transitiva, es decir, no importa lo largo que sea la
cadena de invocaciones entre un mtodo y otro para decir que el mtodo A depende de B.
Por ejemplo, en el siguiente fragmento de cdigo a.x() depende de b.y():
Listado 4.1: Dependencia directa entre mtodos (ejemplo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class B {
void y() {
// ...
}
}
class A {
B b;
void x() {
b.y();
// ...
}
}
main () {
A a;
a.x();
}
[116]
class B {
void y() {
// ...
}
}
class C {
B b;
void z() {
b.y();
}
}
class A {
C c;
void x() {
c.z();
// ...
}
}
main () {
A a;
a.x();
}
De igual forma, sigue habiendo una relacin de dependencia entre a.x() y b.y(), esta
vez por un camino ms largo (y que podra introducir errores). Sin embargo, en esencia,
existe un elemento bsico comn en el diseo de las dos aplicaciones anteriores y es esa
relacin entre los mtodos. En estos ejemplos es muy evidente, pero ocurre con frecuencia
en cdigo de aplicaciones reales donde las relaciones de diseo son ms simples de lo que
a primera vista parece.
Object similar
Object dissimilar
Method Similar
Recursion
Redirection
Method Dissimilar
Conglomeration
Delegation
Tabla 4.2: Patrones de diseo elementales en la relacin method-method (versin simplificada) [8]. No se tiene
en cuenta la tercera componente correspondiente al tipo
[117]
class WheelMaker {
Wheel makeWheel() {
// do the wheel and return it
}
}
class CarMaker {
WheelMaker w;
Car makeCar() {
Wheel wheel1 = w.makeWheel();
// ... do more stuff
}
}
main() {
WheelMaker w;
w.makeCar();
}
El mtodo print() es el mismo en trminos del trabajo que hay que realizar de cara al cliente, es decir, al usuario de PrinterManager. Ambos imprimen un documento y
aunque estn definidos en diferentes tipos, el diseo nos apunta a que el resultado es un
documento impreso. Por ello, cuando un objeto invoca el mismo mtodo de otro est
redirigiendo su trabajo a otra instancia para que lo haga por l.
Listado 4.4: Redirection(ejemplo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Printer {
void print(Document d) {
//...
}
}
class PrinterManager {
Printer p;
void print(Document d) {
p.print(d);
//...
}
}
main() {
Document d;
PrinterManager m;
m.print(d);
}
[118]
En este caso, claramente fabricar una rueda no es lo mismo que fabricar un coche.
Por ello, los mtodos makeWheel() y makeCar() son completamente diferentes, por lo que
no se puede hablar de una redireccin de CarMaker en WheelMaker sino ms bien de una
delegacin. El fabricante de un coche no puede tener todo el conocimiento necesario
para crear cada componente del mismo. Sabe crear coches a partir de unas piezas bsicas
(ruedas, tornillos, etc.) pero no sabe fabricar las piezas. Por ello, delega funcionalidad en
otros.
Esto son solo 4 patrones elementales que se deducen a partir de la relacin existente
entre dos mtodos. Todos los patrones que vienen a continuacin se pueden expresar
como una combinacin de patrones elementales. Para ver la coleccin completa y cmo
diseccionar patrones de alto nivel en componentes elementales se recomienda la lectura
de [8].
4.3.
Patrones de creacin
4.3.1. Singleton
El patrn singleton se suele utilizar cuando se requiere tener una nica instancia de
un determinado tipo de objeto.
Problema
Singleton
-instance: Singleton
+instance(): static Singleton
-Singleton()
Solucin
Para garantizar que slo existe una instancia de una clase es necesario que los clientes
no puedan acceder directamente al constructor. Por ello, en un singleton el constructor es,
por lo menos, protected. A cambio se debe proporcionar un nico punto (controlado) por
el cual se pide la instancia nica. El diagrama de clases de este patrn se muestra en la
figura 4.1.
[119]
Implementacin
A continuacin, se muestra una implementacin bsica del patrn Singleton:
Listado 4.5: Singleton (ejemplo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Header */
class Ball {
protected:
float _x, _y;
static Ball* theBall_;
Ball(float x, float y) : _x(x), _y(y) { };
Ball(const Ball& ball);
void operator=(const Ball& ball ) ;
public:
static Ball& getTheBall();
void move(float _x, float _y) { /*...*/ };
};
Ball& Ball::getTheBall()
{
static Ball theBall_;
return theBall_;
}
Como se puede ver, la caracterstica ms importante es que los mtodos que pueden
crear una instancia de Ball son todos privados para los clientes externos. Todos ellos
deben utilizar el mtodo esttico getTheBall() para obtener la nica instancia. Esta implementacin no es vlida para programas multihilo, es decir, no es thread-safe.
Como ejercicio se plantea la siguiente pregunta: en la implementacin proporcionada,
se garantiza que no hay memory leak? Qu sera necesario para que la implementacin
fuera thread-safe?
Consideraciones
El patrn Singleton puede ser utilizado para modelar:
Gestores de acceso a base de datos, sistemas de ficheros, render de grficos, etc.
Estructuras que representan la configuracin del programa para que sea accesible
por todos los elementos en cualquier instante.
El Singleton es un caso particular de un patrn de diseo ms general llamado Object
Pool, que permite crear n instancias de objetos de forma controlada.
La idoneidad del patrn Singleton es muy controvertida y est muy cuestionada.
Muchos autores y desarrolladores, entre los que destaca Eric Gamma (uno de los
autores de [4]) consideran que es un antipatrn, es decir, una mala solucin a un
problema de diseo.
[120]
Shotgun
+shot()
+reload()
Weapon
Char
+shot()
+reload()
+life()
+move()
Bow
Rie
+shot()
+reload()
+shot()
+reload()
Archer
+life()
+move()
+shot()
Soldier
Villager
+shot()
+work()
Rider
+life()
+move()
+shot()
Farmer
Miner
+life()
+move()
+work()
+life()
+move()
+work()
En ella, se muestra jerarquas de clases que modelan los diferentes tipos de personajes
de un juego y algunas de sus armas. Para construir cada tipo de personaje es necesario
saber cmo construirlo y con qu otro tipo de objetos tiene relacin. Por ejemplo, restricciones del tipo la gente del pueblo no puede llevar armas o los arqueros slo pueden
puede tener un arco, es conocimiento especfico de la clase que se est construyendo.
Supongamos que en nuestro juego, queremos obtener razas de personajes: hombres y
orcos. Cada raza tiene una serie de caractersticas propias que hacen que pueda moverse
ms rpido, trabajar ms o tener ms resistencia a los ataques.
El patrn Abstract Factory puede ser de ayuda en este tipo de situaciones en las que es
necesario crear diferentes tipos de objetos utilizando una jerarqua de componentes. Dada
la complejidad que puede llegar a tener la creacin de una instancia es deseable aislar la
forma en que se construye cada clase de objeto.
Solucin
En la figura 4.3 se muestra la aplicacin del patrn para crear las diferentes razas de
soldados. Por simplicidad, slo se ha aplicado a esta parte de la jerarqua de personajes.
En primer lugar se define una factora abstracta que ser la que utilice el cliente (Game)
para crear los diferentes objetos. CharFactory es una factora que slo define mtodos
abstractos y que sern implementados por sus clases hijas. stas son factoras concretas
a cada tipo de raza (ManFactory y OrcFactory) y ellas son las que crean las instancias
concretas de objetos Archer y Rider para cada una de las razas.
[121]
/* ... */
Game game;
SoldierFactory* factory;
if (isSelectedMan) {
factory = new ManFactory();
} else {
factory = new OrcFactory();
}
game->createSoldiers(factory);
/* ... */
/* Game implementation */
vector<Soldier*> Game::createSoldiers(SoldierFactory* factory)
{
vector<Solider*> soldiers;
for (int i=0; i<5; i++) {
soldiers.push_back(factory->makeArcher());
soldiers.push_back(factory->makeRider());
}
return soldiers;
}
Como puede observarse, la clase Game simplemente invoca los mtodos de la factora
abstracta. Por ello, createSoldier() funciona exactamente igual para cualquier tipo de
factora concreta (de hombres o de orcos).
[122]
makeArcher()
/* OrcFactory */
Archer* OrcFactory::makeArcher()
{
Archer archer = new Archer();
archer->setLife(200);
archer->setName(Orc);
return archer;
}
/* ManFactory */
Archer* ManFactory::makeArcher()
{
Archer archer = new Archer();
archer->setLife(100);
archer->setName(Man);
return archer;
}
Ntese como las factoras concretas ocultan las particularidades de cada tipo. Una
implementacin similar tendra el mtodo makeRider().
Consideraciones
El patrn Abstract Factory puede ser aplicable cuando:
el sistema de creacin de instancias debe aislarse.
es necesaria la creacin de varias instancias de objetos para tener el sistema configurado.
la creacin de las instancias implican la imposicin de restricciones y otras particularidades propias de los objetos que se construyen.
los productos que se deben fabricar en las factoras no cambian excesivamente en
el tiempo. Aadir nuevos productos implica aadir mtodos a todas las factoras
ya creadas, por lo que no es un patrn escalable y que se adapte bien al cambio.
En nuestro ejemplo, si quisiramos aadir un nuevo tipo de soldado deberamos
modificar la factora abstracta y las concretas. Por ello, es recomendable que se
aplique este patrn sobre diseos con un cierto grado de estabilidad.
Un patrn muy similar a ste es el patrn Builder. Con una estructura similar, el patrn
Builder se centra en el proceso de cmo se crean las instancias y no en la jerarqua de
factoras que lo hacen posible. Como ejercicio se plantea estudiar el patrn Builder y
encontrar las diferencias.
[123]
Char
+life()
+move()
SoldierFactory
Soldier
Game
+makeArcher()
+makerRider()
+shot()
ManFactory
OrcFactory
Archer
+makeArcher()
+makeRider()
+makeArcher()
+makeRider()
+life()
+move()
+shot()
Rider
+life()
+move()
+shot()
Problema
Al igual que ocurre con el patrn Abstract Factory, el problema que se pretende resolver es la creacin de diferentes instancias de objetos abstrayendo la forma en que
realmente se crean.
Solucin
La figura 4.4 muestra un diagrama de clases para nuestro ejemplo que emplea el patrn
Factory Method para crear ciudades en las que habitan personajes de diferentes razas.
Como puede verse, los objetos de tipo Village tienen un mtodo populate() que es
implementado por las subclases. Este mtodo es el que crea las instancias de Villager
correspondientes a cada raza. Este mtodo es el mtodo factora. Adems de este mtodo, tambin se proporcionan otros como population() que devuelve la poblacin total, o
location() que devuelve la posicin de la cuidad en el mapa. Todos estos mtodos son
comunes y heredados por las ciudades de hombres y orcos.
Finalmente, objetos Game podran crear ciudades y, consecuentemente, crear ciudadanos de distintos tipos de una forma transparente.
Consideraciones
Este patrn presenta las siguientes caractersticas:
No es necesario tener una factora o una jerarqua de factoras para la creacin de
objetos. Permite diseos ms adaptados a la realidad.
[124]
Village
Villager
+populate()
+population()
+location()
Game
+work()
ManVillage
OrcVillage
Farmer
Miner
+populate()
+populate()
+life()
+move()
+work()
+life()
+move()
+work()
El mtodo factora, al estar integrado en una clase, hace posible conectar dos jerarqua de objetos distintas. Por ejemplo, si los personajes tienen un mtodo factora
de las armas que pueden utilizar, el dominio de las armas y los personajes queda
unido a travs el mtodo. Las subclases de los personajes crearan las instancias de
Weapon correspondientes.
Ntese que el patrn Factory Method se utiliza para implementar el patrn Abstract
Factory ya que la factora abstracta define una interfaz con mtodos de construccin de
objetos que son implementados por las subclases.
4.3.4. Prototype
El patrn Prototype proporciona abstraccin a la hora de crear diferentes objetos en un
contexto donde se desconoce cuntos y cules deben ser creados a priori. La idea principal
es que los objetos deben poder clonarse en tiempo de ejecucin.
Problema
Los patrones Factory Method y Abstract Factory tienen el problema de que se basan
en la herencia e implementacin de mtodos abstractos por subclases para definir cmo se
construye cada producto concreto. Para sistemas donde el nmero de productos concretos
puede ser elevado o indeterminado esto puede ser un problema.
Supongamos que en nuestra jerarqua de armas, cuya clase padre es Weapon, comienza
a crecer con nuevos tipos de armas y, adems, pensamos en dejar libertad para que se
carguen en tiempo de ejecucin nuevas armas que se implementarn como libreras dinmicas. Adems, el nmero de armas variar dependiendo de ciertas condiciones del juego
y de configuracin. En este contexto, puede ser ms que dudoso el uso de factoras.
[125]
Solucin
Para atender a las nuevas necesidades dinmicas en la creacin de los distintos tipo
de armas, sin perder la abstraccin sobre la creacin misma, se puede utilizar el patrn
Prototype como se muestra en la figura 4.5.
Weapon
prototype
+shot()
+reload()
+clone()
Client
+method()
...
instance = prototype->clone()
...
Shotgun
+shot()
+reload()
+clone()
Bow
Rie
+shot()
+reload()
+clone()
+shot()
+reload()
+clone()
4.4.
Patrones estructurales
Hasta ahora, hemos visto patrones para disear aplicaciones donde el problema principal es la creacin de diferentes instancias de clases. En esta seccin se mostrarn los
patrones de diseo estructurales que se centran en las relaciones entre clases y en cmo
organizarlas para obtener un diseo eficiente para resolver un determinado problema.
[126]
4.4.1. Composite
El patrn Composite se utiliza para crear una organizacin arbrea y homognea de
instancias de objetos.
Problema
Para ilustrar el problema supngase un juego de estrategia en el que los jugadores
pueden recoger objetos o items, los cuales tienen una serie de propiedades como precio,
descripcin, etc. Cada item, a su vez, puede contener otros items. Por ejemplo, un bolso
de cuero puede contener una pequea caja de madera que, a su vez, contiene un pequeo
reloj dorado.
En definitiva, el patrn Composite habla sobre cmo disear este tipo de estructuras
recursivas donde la composicin homognea de objetos recuerda a una estructura arbrea.
Solucin
Para el ejemplo expuesto anteriormente, la aplicacin del patrn Composite quedara
como se muestran en la figura 4.6. Como se puede ver, todos los elementos son Items que
implementan una serie de mtodos comunes. En la jerarqua existen objetos compuestos, como Bag, que mantienen una lista (items) donde residen los objetos que contiene.
Naturalmente, los objetos compuestos suelen ofrecer tambin operaciones para aadir,
eliminar y actualizar. Por otro lado, hay objetos hoja que no contienen a ms objetos,
como es el caso de Clock.
Item
+value()
+description()
Clock
Bag
+value()
+description()
+value()
+description()
items
Consideraciones
Al utilizar este patrn, se debe tener en cuenta las siguientes consideraciones:
Una buena estrategia para identificar la situacin en la que aplicar este patrn es
cuando tengo un X y tiene varios objetos X.
La estructura generada es muy flexible siempre y cuando no importe el tipo de objetos que pueden tener los objetos compuestos. Es posible que sea deseable prohibir
la composicin de un tipo de objeto con otro. Por ejemplo, un jarrn grande dentro de una pequea bolsa. La comprobacin debe hacerse en tiempo de ejecucin
y no es posible utilizar el sistema de tipos del compilador. En este sentido, usando
Composite se relajan las restricciones de composicin entre objetos.
[127]
Los usuarios de la jerarqua se hacen ms sencillos, ya que slo tratan con un tipo
abstracto de objeto, dndole homogeneidad a la forma en que se maneja la estructura.
4.4.2. Decorator
Tambin conocido como Wrapper, el patrn Decorator sirve para aadir y/o modificar
la responsabilidad, funcionalidad o propiedades de un objeto en tiempo de ejecucin.
Problema
Supongamos que el personaje de nuestro videojuego porta un arma que utiliza para
eliminar a sus enemigos. Dicha arma, por ser de un tipo determinado, tiene una serie de
propiedades como el radio de accin, nivel de ruido, nmero de balas que puede almacenar, etc. Sin embargo, es posible que el personaje incorpore elementos al arma que puedan
cambiar estas propiedades como un silenciador o un cargador extra.
El patrn Decorator permite organizar el diseo de forma que la incorporacin de
nueva funcionalidad en tiempo de ejecucin a un objeto sea transparente desde el punto
de vista del usuario de la clase decorada.
Solucin
En la figura 4.7 se muestra la aplicacin del patrn Decorator al supuesto anteriormente descrito. Bsicamente, los diferentes tipos de armas de fuego implementan una
clase abstracta llamada Firearm. Una de sus hijas es FirearmDecorator que es el padre
de todos los componentes que decoran a un objeto Firearm. Ntese que este decorador
implementa la interfaz propuesta por Firearm y est compuesta por un objeto gun, el cual
decora.
Implementacin
A continuacin, se expone una implementacin en C++ del ejemplo del patrn Decorator. En el ejemplo, un arma de tipo Rifle es decorada para tener tanto silenciador como
una nueva carga de municin. Ntese cmo se utiliza la instancia gun a lo largo de los
constructores de cada decorador.
[128]
class Firearm {
public:
virtual float noise() const = 0;
virtual int bullets() const = 0;
};
class Rifle : public Firearm {
public:
float noise () const { return 150.0; }
int bullets () const { return 5; }
};
/* Decorators */
class FirearmDecorator : public Firearm {
protected:
Firearm* _gun;
public:
FirearmDecorator(Firearm* gun): _gun(gun) {};
virtual float noise () const { return _gun->noise(); }
virtual int bullets () const { return _gun->bullets(); }
};
class Silencer : public FirearmDecorator {
public:
Silencer(Firearm* gun) : FirearmDecorator(gun) {};
float noise () const { return _gun->noise() - 55; }
int bullets () const { return _gun->bullets(); }
};
class Magazine : public FirearmDecorator {
public:
Magazine(Firearm* gun) : FirearmDecorator(gun) {};
float noise () const { return _gun->noise(); }
int bullets () const { return _gun->bullets() + 5; }
};
/* Using decorators */
...
Firearm* gun = new Rifle();
cout << "Noise: " << gun->noise() << endl;
cout << "Bullets: " << gun->bullets() << endl;
...
// char gets a silencer
gun = new Silencer(gun);
cout << "Noise: " << gun->noise() << endl;
cout << "Bullets: " << gun->bullets() << endl;
...
// char gets a new magazine
gun = new Magazine(gun);
cout << "Noise: " << gun->noise() << endl;
cout << "Bullets: " << gun->bullets() << endl;
[129]
Consideraciones
A la hora de aplicar el patrn Decorator se deben tener en cuenta las siguientes consideraciones:
Es un patrn similar al Composite. Sin embargo, existen grandes diferencias:
Est ms centrado en la extensin de la funcionalidad que en la composicin
de objetos para la generacin de una jerarqua como ocurre en el Composite.
Normalmente, slo existe un objeto decorado y no un vector de objetos (aunque tambin es posible).
Este patrn permite tener una jerarqua de clases compuestas, formando una estructura ms dinmica y flexible que la herencia esttica. El diseo equivalente
utilizando mecanismos de herencia debera considerar todos los posibles casos en
las clases hijas. En nuestro ejemplo, habra 4 clases: rifle, rifle con silenciador, rifle
con cargador extra y rifle con silenciador y cargador. Sin duda, este esquema es
muy poco flexible.
Firearm
+noise()
+bullets()
Rie
+noise()
+bullets()
FirearmDecorator
gun
+noise()
+bullets()
Silencer
+noise()
+bullets()
Magazine
+noise()
+bullets()
4.4.3. Facade
El patrn Facade eleva el nivel de abstraccin de un determinado sistema para ocultar
ciertos detalles de implementacin y hacer ms sencillo su uso.
Problema
Muchos de los sistemas que proporcionan la capacidad de escribir texto en pantalla
son complejos de utilizar. Su complejidad reside en su naturaleza generalista, es decir,
estn diseados para abarcar un gran nmero de tipos de aplicaciones. Por ello, el usuario
normalmente debe considerar cuestiones de bajo nivel como es configurar el propio
sistema interconectando diferentes objetos entre s que, a priori, parece que nada tienen
que ver con la tarea que se tiene que realizar.
[130]
Para ver el problema que supone para un usuario un bajo nivel de abstraccin no
es necesario recurrir a una librera o sistema externo. Nuestro propio proyecto, si est
bien diseado, estar dividido en subsistemas que proporcionan una cierta funcionalidad.
Basta con que sean genricos y reutilizables para que su complejidad aumente considerablemente y, por ello, su uso sea cada vez ms tedioso.
Por ejemplo, supongamos que hemos creado diferentes sistemas para realizar distintas
operaciones grficas (manejador de archivos, cargador de imgenes, etc.). El siguiente
cdigo correspondera con la animacin de una explosin en un punto determinado de la
pantalla.
6
8
screen->add_element(explosion1, x, y);
screen->add_element(explosion2, x+2, y+2);
...
11
/* more configuration */
...
12
screen->draw();
10
Sin duda alguna, y pese a que ya se tienen objetos que abstraen subsistemas tales como sistemas de archivos, para los clientes que nicamente quieran mostrar explosiones
no proporciona un nivel de abstraccin suficiente. Si esta operacin se realiza frecuentemente, el cdigo se repetir a lo largo y ancho de la aplicacin y problema se agrava.
Solucin
Utilizando el patrn Facade, se proporciona un mayor nivel de abstraccin al cliente
de forma que se construye una clase fachada entre l y los subsistemas con menos
nivel de abstraccin. De esta forma, se proporciona una visin unificada del conjunto y,
adems, se controla el uso de cada componente.
Para el ejemplo anterior, se podra crear una clase que proporcione la una funcionalidad ms abstracta. Por ejemplo, algo parecido a lo siguiente::
Listado 4.10: Simplificacin utilizando Facade
1
2
Como se puede ver, el usuario ya no tiene que conocer las relaciones que existen entre
los diferentes mdulos para crear este tipo de animaciones. Esto aumenta, levemente, el
nivel de abstraccin y hace ms sencillo su uso.
En definitiva, el uso del patrn Facade proporciona una estructura de diseo como la
mostrada en la figura 4.8.
[131]
Facade
Consideraciones
El patrn Facade puede ser til cuando:
Es necesario refactorizar, es decir, extraer funcionalidad comn de los sistemas y
agruparla en funcin de las necesidades.
Los sistemas deben ser independientes y portables.
Se required controlar el acceso y la forma en que se utiliza un sistema determinado.
Los clientes pueden seguir utilizando los subsistemas directamente, sin pasar por la
fachada, lo que da la flexibilidad de elegir entre una implementacin de bajo nivel
o no.
Sin embargo, utilizando el patrn Facade es posible caer en los siguientes errores:
Crear clases con un tamao desproporcionado. Las clases fachada pueden contener
demasiada funcionalidad si no se divide bien las responsabilidades y se tiene claro
el objetivo para el cual se creo la fachada. Para evitarlo, es necesario ser crtico/a
con el nivel de abstraccin que se proporciona.
Obtener diseos poco flexibles y con mucha contencin. A veces, es posible crear
fachadas que obliguen a los usuarios a un uso demasiado rgido de la funcionalidad
que proporciona y que puede hacer que sea ms cmodo, a la larga, utilizar los subsistemas directamente. Adems, una fachada puede convertirse en un nico punto
de fallo, sobre todo en sistemas distribuidos en red.
Exponer demasiados elementos y, en definitiva, no proporcionar un nivel de abstraccin adecuado.
4.4.4.
MVC
El patrn MVC (Model View Controller) se utiliza para aislar el dominio de aplicacin, es decir, la lgica, de la parte de presentacin (interfaz de usuario).
[132]
Problema
Programas como los videojuegos requieren la interaccin de un usuario que, normalmente, realiza diferentes acciones sobre una interfaz grfica. Las interfaces disponibles
son muy variadas: desde aplicaciones de escritorio con un entorno GTK (GIMP ToolKit)
a aplicaciones web, pasando por una interfaz en 3D creada para un juego determinado.
Supongamos que una aplicacin debe soportar varios tipos de interfaz a la vez. Por
ejemplo, un juego que puede ser utilizado con una aplicacin de escritorio y, tambin, a
travs de una pgina web. El patrn MVC sirve para aislar la lgica de la aplicacin, de la
forma en que se sta se presenta, su interfaz grfica.
Solucin
En el patrn MVC, mostrado en la figura 4.9, existen tres entidades bien definidas:
Vista: se trata de la interfaz de usuario que interacta con el usuario y recibe sus
rdenes (pulsar un botn, introducir texto, etc.). Tambin recibe rdenes desde el
controlador, para mostrar informacin o realizar un cambio en la interfaz.
Controlador: el controlador recibe rdenes utilizando, habitualmente, manejadores
o callbacks y traduce esa accin al dominio del modelo de la aplicacin. La accin
puede ser crear una nueva instancia de un objeto determinado, actualizar estados,
pedir operaciones al modelo, etc.
Modelo: el modelo de la aplicacin recibe las acciones a realizar por el usuario,
pero ya independientes del tipo de interfaz utilizado porque se utilizan, nicamente,
estructuras propias del dominio del modelo y llamadas desde el controlador.
Normalmente, la mayora de las acciones que realiza el controlador sobre el modelo
son operaciones de consulta de su estado para que pueda ser convenientemente
representado por la vista.
MVC no es patrn con una separacin tan rgida. Es posible encontrar implementaciones en las que, por ejemplo, el modelo notifique directamente a las interfaces
de forma asncrona eventos producidos en sus estructuras y que deben ser representados en la vista (siempre y cuando exista una aceptable independencia entre las
capas). Para ello, es de gran utilidad el patrn Observer (ver seccin 4.5.1).
Consideraciones
El patrn MVC es la filosofa que se utiliza en un gran
nmero de entornos de ventanas. Sin embargo, muchos
sistemas web como Django tambin se basan en este patrn. Sin duda, la divisin del cdigo en estos roles proporciona flexibilidad a la hora de crear diferentes tipos de
presentaciones para un mismo dominio.
De hecho, desde un punto de vista general, la estrucFigura 4.9: Estructura del patrn
tura ms utilizada en los videojuegos se asemeja a un paMVC.
trn MVC: la interfaz grfica utilizando grficos 3D/2D
(vista), bucle de eventos (controlador) y las estructuras de datos internas (modelo).
[133]
4.4.5. Adapter
El patrn Adapter se utiliza para proporcionar una interfaz que, por un lado, cumpla
con las demandas de los clientes y, por otra, haga compatible otra interfaz que, a priori,
no lo es.
Problema
Es muy probable que conforme avanza la construccin de la aplicacin, el diseo de
las interfaces que ofrecen los componentes pueden no ser las adecuadas o, al menos, las
esperadas por los usuarios de los mismos. Una solucin rpida y directa es adaptar dichas
interfaces a nuestras necesidades. Sin embargo, esto puede que no sea tan sencillo.
En primer lugar, es posible que no tengamos la posibilidad de modificar el cdigo de
la clase o sistema que pretendemos cambiar. Por otro lado, puede ser que sea un requisito
no funcional por parte del cliente: determinado sistema o biblioteca debe utilizarse s o
s. Si se trata de una biblioteca externa (third party), puede ocurrir que la modificacin
suponga un coste adicional para el proyecto ya que tendra que ser mantenida por el propio
proyecto y adaptar las mejoras y cambios que se aadan en la versin no modificada.
Contando, claro, con que la licencia de la biblioteca permita todo esto.
Por lo tanto, es posible llegar a la conclusin de que a pesar de que el sistema, biblioteca o clase no se adapta perfectamente a nuestras necesidades, trae ms a cuenta utilizarla
que hacerse una versin propia.
Solucin
Usando el patrn Adapter es posible crear una nueva interfaz de acceso a un determinado objeto, por lo que proporciona un mecanismo de adaptacin entre las demandas del
objeto cliente y el objeto servidor que proporciona la funcionalidad.
Client
Target
+method()
Adapter
adaptee
+method()
OtherSystem
+otherMethod()
adaptee->otherMethod()
[134]
Consideraciones
Algunas consideraciones sobre el uso del patrn Adapter:
Tener sistemas muy reutilizables puede hacer que sus interfaces no puedan ser compatibles con una comn. El patrn Adapter es una buena opcin en este caso.
Un mismo adaptador puede utilizarse con varios sistemas.
Otra versin del patrn es que la clase Adapter sea una subclase del sistema adaptado. En este caso, la clase Adapter y la adaptada tienen una relacin ms estrecha
que si se realiza por composicin.
Este patrn se parece mucho al Decorator. Sin embargo, difieren en que la finalidad
de ste es proporcionar una interfaz completa del objeto adaptador, mientras que el
decorador puede centrarse slo en una parte.
4.4.6. Proxy
El patrn Proxy proporciona mecanismos de abstraccin y control para acceder a un
determinado objeto simulando que se trata del objeto real.
Problema
Muchos de los objetos de los que puede constar una aplicacin pueden presentar diferentes problemas a la hora de ser utilizados por clientes:
Coste computacional: es posible que un objeto, como una imagen, sea costoso de
manipular y cargar.
Acceso remoto: el acceso por red es una componente cada vez ms comn entre las
aplicaciones actuales. Para acceder a servidores remotos, los clientes deben conocer
las interioridades y pormenores de la red (sockets, protocolos, etc.).
Acceso seguro: es posible que muchos objetos necesiten diferentes privilegios para
poder ser utilizados. Por ejemplo, los clientes deben estar autorizados para poder
acceder a ciertos mtodos.
Dobles de prueba: a la hora de disear y probar el cdigo, puede ser til utilizar
objetos dobles que reemplacen instancias reales que pueden hacer que las pruebas
sea pesadas y/o lentas.
Solucin
Supongamos el problema de mostrar una imagen cuya carga es costosa en trminos
computacionales. La idea detrs del patrn Proxy (ver figura 4.11) es crear una un objeto
intermedio (ImageProxy) que representa al objeto real (Image) y que se utiliza de la misma
forma desde el punto de vista del cliente. De esta forma, el objeto proxy puede cargar una
nica vez la imagen y mostrarla tantas veces como el cliente lo solicite.
[135]
Graphic
+display()
Image
image
+display()
ImageProxy
+display()
Client
Implementacin
A continuacin, se muestra una implementacin del problema anteriormente descrito
donde se utiliza el patrn Proxy. En el ejemplo puede verse (en la parte del cliente) cmo
la imagen slo se carga una vez: la primera vez que se invoca a display(). El resto de
invocaciones slo muestran la imagen ya cargada.
Listado 4.11: Ejemplo de implementacin de Proxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Graphic {
public:
void display() = 0;
};
class Image : public Graphic {
public:
void load() {
...
/* perform a hard file load */
...
}
void display() {
...
/* perform display operation */
...
}
};
class ImageProxy : public Graphic {
private:
Image* _image;
public:
void display() {
if (not _image) {
_image = new Image();
_image.load();
}
_image->display();
}
};
/* Client */
...
Graphic image = new ImageProxy();
image->display(); // loading and display
image->display(); // just display
image->display(); // just display
...
[136]
Consideraciones
Existen muchos ejemplos donde se hace un uso intensivo del patrn proxy en diferentes sistemas:
En los sistemas de autenticacin, dependiendo de las credenciales presentadas por
el cliente, devuelven un proxy u otro que permiten realizar ms o menos operaciones.
En middlewares orientados a objetos como CORBA o ZeroC I CE (Internet Communication Engine), se utiliza la abstraccin del Proxy para proporcionar invocaciones
remotas entre objetos distribuidos. Desde el punto de vista del cliente, la invocacin
se produce como si el objeto estuviera accesible localmente. El proxy es el encargado de proporcionar esta abstraccin.
4.5.
Patrones de comportamiento
4.5.1. Observer
El patrn Observer se utiliza para definir relaciones 1 a n de forma que un objeto
pueda notificar y/o actualizar el estado de otros automticamente.
Problema
Tal y como se describi en la seccin 4.4.4, el dominio de la aplicacin en el patrn
MVC puede notificar de cambios a las diferentes vistas. Es importante que el dominio no
conozca los tipos concretos de las vistas, de forma que haya que modificar el dominio en
caso de aadir o quitar una vista.
Otros ejemplos donde se da este tipo de problemas ocurren cuando el estado un elemento tiene influencia directa sobre otros. Por ejemplo, si en el juego se lanza un misil
seguramente haya que notificar que se ha producido el lanzamiento a diferentes sistemas como el de sonido, partculas, luz, etc. Adems, otros objetos alredor pueden estar
interesados en esta informacin.
Solucin
El patrn Observer proporciona un diseo con poco acoplamiento entre los observadores y el objeto observado. Siguiendo la filosofa de publicacin/suscripcin, los objetos
observadores se deben registrar en el objeto observado, tambin conocido como subject.
As, cuando ocurra el evento oportuno, el subject recibir una invocacin a travs de
notify() y ser el encargado de notificar a todos los elementos suscritos a l a travs
del mtodo update(). Los observadores que reciben la invocacin pueden realizar las acciones pertinentes como consultar el estado del dominio para obtener nuevos valores. En
la figura 4.12 se muestra un esquema general del patrn Observer.
[137]
Subject
for ob in observers:
ob->update()
+attach()
+detach()
+notify()
observers
ConcreteSubject
+state()
Observer
+update()
subject
ConcreteObserver
+update()
return the_state
subject->state()
Consideraciones
Al emplear el patrn Observer en nuestros diseos, se deben tener en cuenta las siguientes consideraciones:
[138]
4.5.2. State
El patrn State es til para realizar transiciones de estado e implementar autmatas
respetando el principio de encapsulacin.
Problema
Es muy comn que en cualquier aplicacin, incluido los videojuegos, existan estructuras que pueden ser modeladas directamente como un autmata, es decir, una coleccin de
estados y unas transiciones dependientes de una entrada. En este caso, la entrada pueden
ser invocaciones y/o eventos recibidos.
Por ejemplo, los estados de un personaje de un videojuego podran ser: de pie,
tumbado, andando y saltando. Dependiendo del estado en el que se encuentre y
de la invocacin recibida, el siguiente estado ser uno u otro. Por ejemplo, si est de pie
y recibe la orden de tumbarse, sta se podr realizar. Sin embargo, si ya est tumbado
no tiene sentido volver a tumbarse, por lo que debe permanecer en ese estado. Tampoco
podra no tener sentido pasar al estado de saltando directamente desde tumbado sin
pasar antes por de pie.
Solucin
El patrn State permite encapsular el mecanismo de las transiciones que sufre un
objeto a partir de los estmulos externos. En la figura 4.14 se muestra un ejemplo de
aplicacin del mismo. La idea es crear una clase abstracta que representa al estado del
personaje (CharacterState). En ella se definen las mismas operaciones que puede recibir
el personaje con una implementacin por defecto. En este caso, la implementacin es
vaca.
[139]
Default implementation
is "do nothing".
Character
+walk()
+getUp()
+getDown()
+jump()
CharacterState
+walk()
+getUp()
+getDown()
+jump()
state
state->jump(this)
CharacterStanding
CharacterLying
+walk()
+getDown()
+jump()
+getUp()
char->setState(CharacterJumping())
CharacterJumping
CharacterWalking
+getUp()
+jump()
Por cada estado en el que puede encontrarse el personaje, se crea una clase que hereda de la clase abstracta anterior, de forma que en cada una de ellas se implementen los
mtodos que producen cambio de estado.
Por ejemplo, segn el diagrama, en el estado de pie se puede recibir la orden de caminar, tumbarse y saltar, pero no de levantarse. En caso de recibir esta ltima, se ejecutar
la implementacin por defecto, es decir, no hacer nada.
En definitiva, la idea es que las clases que representan a los estados sean las encargadas de cambiar el estado del personaje, de forma que los cambios de estados quedan
encapsulados y delegados al estado correspondiente.
Consideraciones
Los componentes del diseo que se comporten como autmatas son buenos candidatos a ser modelados con el patrn State. Una conexin TCP (Transport Control
Protocol) o un carrito en una tienda web son ejemplos de este tipo de problemas.
Es posible que una entrada provoque una situacin de error estando en un determinado estado. Para ello, es posible utilizar las excepciones para notificar dicho
error.
Las clases que representan a los estados no deben mantener un estado intrnseco,
es decir, no se debe hacer uso de variables que dependan de un contexto. De esta
forma, el estado puede compartirse entre varias instancias. La idea de compartir un
estado que no depende del contexto es la base fundamental del patrn Flyweight,
que sirve para las situaciones en las que crear muchas instancias puede ser un problema de rendimiento.
4.5.3. Iterator
El patrn Iterator se utiliza para ofrecer una interfaz de acceso secuencial a una determinada estructura ocultando la representacin interna y la forma en que realmente se
accede.
[140]
+add()
+remove()
+iterator()
ConcreteStruct
+add()
+remove()
+iterator()
+next()
+first()
+end()
+isDone()
ConcreteIterator
+next()
+first()
+end()
+isDone()
Problema
Manejar colecciones de datos es algo muy habitual en el desarrollo de aplicaciones.
Listas, pilas y, sobre todo, rboles son ejemplos de estructuras de datos muy presentes en
los juegos y se utilizan de forma intensiva.
Una operacin muy frecuente es recorrer las estructuras para analizar y/o buscar los
datos que contienen. Es posible que sea necesario recorrer la estructura de forma secuencial, de dos en dos o, simplemente, de forma aleatoria. Los clientes suelen implementar
el mtodo concreto con el que desean recorrer la estructura por lo que puede ser un problema si, por ejemplo, se desea recorrer una misma estructura de datos de varias formas
distintas. Conforme aumenta las combinaciones entre los tipos de estructuras y mtodos
de acceso, el problema se agrava.
Solucin
Con ayuda del patrn Iterator es posible obtener acceso secuencial, desde el punto de
vista del usuario, a cualquier estructura de datos, independientemente de su implementacin interna. En la figura 4.15 se muestra un diagrama de clases genrico del patrn.
Como puede verse, la estructura de datos es la encargada de crear el iterador adecuado
para ser accedida a travs del mtodo iterator(). Una vez que el cliente ha obtenido el
iterador, puede utilizar los mtodos de acceso que ofrecen tales como next() (para obtener
el siguiente elemento) o isDone() para comprobar si no existen ms elementos.
Implementacin
A continuacin, se muestra una implementacin simplificada y aplicada a una estructura de datos de tipo lista. Ntese cmo utilizando las primitivas que ofrece la estructura,
el iterador proporciona una visin de acceso secuencial a travs del mtodo next().
[141]
Consideraciones
La ventaja fundamental de utilizar el patrn Iterator es la simplificacin de los clientes
que acceden a las diferentes estructuras de datos. El control sobre el acceso lo realiza el
propio iterador y, adems, almacena todo el estado del acceso del cliente. De esta forma
se crea un nivel de abstraccin para los clientes que acceden siempre de la misma forma
a cualquier estructura de datos con iteradores.
Obviamente, nuevos tipos de estructuras de datos requieren nuevos iteradores. Sin embargo, para aadir nuevos tipos de iteradores a estructuras ya existentes puede realizarse
de dos formas:
[142]
La STL de C++ implementa el patrn Iterator en todos los contenedores que ofrece.
[143]
GamePlayer
if moveFirst():
doBestMove();
+play()
+doBestMove()
+isOver()
+moveFirst()
while !isOver():
waitForOpponent();
if (!isOver());
doBestMove();
ChessPlayer
CheckersPlayer
+doBestMove()
+isOver()
+moveFirst()
+doBestMove()
+isOver()
+moveFirst()
return isCheckmate();
return noMoreChecker();
4.5.5. Strategy
El patrn Strategy se utiliza para encapsular el funcionamiento de una familia de algoritmos, de forma que se pueda intercambiar su uso sin necesidad de modificar a los
clientes.
Problema
En muchas ocasiones, se suele proporcionar diferentes algoritmos para realizar una
misma tarea. Por ejemplo, el nivel de habilidad de un jugador viene determinado por
diferentes algoritmos y heursticas que determinan el grado de dificultad. Utilizando diferentes tipos algoritmos podemos obtener desde jugadores que realizan movimientos
aleatorios hasta aquellos que pueden tener cierta inteligencia y que se basan en tcnicas
de IA.
Lo deseable sera poder tener jugadores de ambos tipos y que, desde el punto de vista
del cliente, no fueran tipos distintos de jugadores. Simplemente se comportan diferente
porque usan distintos algoritmos internamente, pero todos ellos son jugadores.
Solucin
Mediante el uso de la herencia, el patrn Strategy permite encapsular diferentes algoritmos para que los clientes puedan utilizarlos de forma transparente. En la figura 4.17
puede verse la aplicacin de este patrn al ejemplo anterior de los jugadores.
[144]
Movement
strategy
+doBestMove()
+move(context)
strategy->move(context);
RandomMovement
IAMovement
+move(context)
+move(context)
La idea es extraer los mtodos que conforman el comportamiento que puede ser intercambiado y encapsularlo en una familia de algoritmos. En este caso, el movimiento
del jugador se extrae para formar una jerarqua de diferentes movimientos (Movement).
Todos ellos implementan el mtodo move() que recibe un contexto que incluye toda la
informacin necesaria para llevar a cabo el algoritmo.
El siguiente fragmento de cdigo indica cmo se usa este esquema por parte de un
cliente. Ntese que al configurarse cada jugador, ambos son del mismo tipo de cara al
cliente aunque ambos se comportarn de forma diferente al invocar al mtodo doBestMove().
Listado 4.13: Uso de los jugadores (Strategy)
1
2
3
4
5
Consideraciones
El patrn Strategy es una buena alternativa a realizar subclases en las entidades que
deben comportarse de forma diferente en funcin del algoritmo utilizado. Al extraer la
heurstica a una familia de algoritmos externos, obtenemos los siguientes beneficios:
Se aumenta la reutilizacin de dichos algoritmos.
Se evitan sentencias condicionales para elegir el comportamiento deseado.
Los clientes pueden elegir diferentes implementaciones para un mismo comportamiento deseado, por lo que es til para depuracin y pruebas donde se pueden
escoger implementaciones ms simples y rpidas.
4.5.6. Reactor
El patrn Reactor es un patrn arquitectural para resolver el problema de cmo atender peticiones concurrentes a travs de seales y manejadores de seales.
[145]
Problema
Existen aplicaciones, como los servidores web, cuyo comportamiento es reactivo, es
decir, a partir de la ocurrencia de un evento externo se realizan todas las operaciones
necesarias para atender a ese evento externo. En el caso del servidor web, una conexin entrante (evento) disparara la ejecucin del cdigo pertinente que creara un hilo
de ejecucin para atender a dicha conexin. Pero tambin pueden tener comportamiento
proactivo. Por ejemplo, una seal interna puede indicar cundo destruir una conexin con
un cliente que lleva demasiado tiempo sin estar accesible.
En los videojuegos ocurre algo muy similar: diferentes entidades pueden lanzar eventos que deben ser tratados en el momento en el que se producen. Por ejemplo, la pulsacin
de un botn en el joystick de un jugador es un evento que debe ejecutar el cdigo pertinente para que la accin tenga efecto en el juego.
Solucin
En el patrn Reactor se definen una serie de actores con las siguientes responsabilidades (vase figura 4.18):
*
Reactor
+regHandler()
+unregHandler()
+loop()
EventHandler
+handle(event)
handle
+getHandle()
event = select();
for h in handlers;
h->handle(event);
ConcreteEventHandler
*
Handle
+handle(event)
+getHandle()
any OS resource
Eventos: los eventos externos que puedan ocurrir sobre los recursos (Handles). Normalmente su ocurrencia es asncrona y siempre est relaciona a un recurso determinado.
Recursos (Handles): se refiere a los objetos sobre los que ocurren los eventos.
La pulsacin de una tecla, la expiracin de un temporizador o una conexin entrante en un socket son ejemplos de eventos que ocurren sobre ciertos recursos.
La representacin de los recursos en sistemas tipo GNU/Linux es el descriptor de
fichero.
Manejadores de Eventos: Asociados a los recursos y a los eventos que se producen
en ellos, se encuentran los manejadores de eventos (EventHandler) que reciben una
invocacin a travs del mtodo handle() con la informacin del evento que se ha
producido.
Reactor: se trata de la clase que encapsula todo el comportamiento relativo a la
demultiplexacin de los eventos en manejadores de eventos (dispatching). Cuando ocurre un cierto evento, se busca los manejadores asociados y se les invoca el
mtodo handle().
[146]
4.5.7. Visitor
El patrn Visitor proporciona un mecanismo para realizar diferentes operaciones sobre una jerarqua de objetos de forma que aadir nuevas operaciones no haga necesario
cambiar las clases de los objetos sobre los que se realizan las operaciones.
Problema
En el diseo de un programa, normalmente se obtienen jerarquas de objetos a travs
de herencia o utilizando el patrn Composite (vase seccin 4.4.1). Considerando una
jerarqua de objetos que sea ms o menos estable, es muy probable que necesitemos realizar operaciones sobre dicha jerarqua. Sin embargo, puede ser que cada objeto deba ser
tratado de una forma diferente en funcin de su tipo. La complejidad de estas operaciones
aumenta muchsimo.
[147]
Client
+visitElementA()
+visitElementB()
Element
+accept(v:Visitor)
ElementA
ElementB
+accept(v:Visitor)
+accept(v:Visitor)
v->visitElementA(this);
ConcreteVisitor1
ConcreteVisitor2
+visitElementA()
+visitElementB()
+visitElementA()
+visitElementB()
v->visitElementB(this);
Implementacin
Como ejemplo de implementacin supongamos que tenemos una escena (Scene) en
la que existe una coleccin de elementos de tipo ObjectScene. Cada elemento tiene atributos como su nombre, peso y posicin en la escena, es decir, name, weight y position,
respectivamente. Se definen dos tipos visitantes:
[148]
BombVisitor:
class ObjectScene {
public:
void accept(SceneVisitor* v) { v->visitObject(this); };
};
class Scene {
private:
vector<ObjectScene> _objects;
public:
void accept(SceneVisitor* v) {
for (vector<ObjectScene>::iterator ob = _objects.begin();
ob != _objects.end(); ob++)
v->accept(v);
v->visitScene(this);
};
};
class SceneVisitor {
virtual void visitObject(ObjectScene* ob) = 0;
virtual void visitScene(Scene* scene) = 0;
};
class NameVisitor : public SceneVisitor {
private:
vector<string> _names;
public:
void visitObject(ObjectScene* ob) {
_names.push_back(ob->name);
};
void visitScene(Scene* scene) {
cout << "The scene " << scene->name << " has following objects:" << endl;
for (vector<string>::iterator it = _names.begin();
it != _names.end(); it++)
cout << *it << endl;
};
};
class BombVisitor : public SceneVisitor {
private:
Bomb _bomb;
public:
BombVisitor(Bomb bomb) : _bomb(bomb);
void visitObject(ObjectScene* ob) {
Point new_pos = calculateNewPosition(ob->position,
ob->weight,
_bomb->intensity);
ob->position = new_pos;
};
void visitScene(ObjectScene* scene) {};
};
Scene* scene = createScene();
SceneVisitor* name_visitor = new NameVisitor();
scene->accept(name_visitor);
...
/* bomb explosion occurs */
SceneVisitor* bomb_visitor = new BombVisitor(bomb);
scene->accept(bomb_visitor);
[149]
Se ha simplificado la implementacin de Scene y ObjectScene. nicamente se ha incluido la parte relativa al patrn Visitor, es decir, la implementacin de los mtodos
accept(). Ntese que es la escena quien que ejecuta accept() sobre todos sus elementos y cada uno de ellos invoca a visitObject(), con una referencia a s mismo para que
el visitante pueda extraer informacin. Dependiendo del tipo de Visitor instanciado, uno
simplemente almacenar el nombre del objeto y el otro calcular si el objeto debe moverse a causa de una determinada explosin. Este mecanismo se conoce como despachado
doble o double dispatching. El objeto que recibe la invocacin del accept() delega la
implementacin de lo que se debe realizar a un tercero, en este caso, al visitante.
Finalmente, la escena tambin invoca al visitante para que realice las operaciones
oportunas una vez finalizado el anlisis de cada objeto. Ntese que, en el ejemplo, en el
caso de BombVisitor no se realiza ninguna accin en este caso.
Consideraciones
Algunas consideraciones sobre el patrn Visitor:
El patrn Visitor es muy conveniente para recorrer estructuras arbreas y realizar
operaciones en base a los datos almacenados.
En el ejemplo, la ejecucin se realiza de forma secuencial ya que se utiliza un
iterador de la clase vector. La forma en que se recorra la estructura influir notablemente en el rendimiento del anlisis de la estructura. Se puede hacer uso del
patrn Iterator para decidir cmo escoger el siguiente elemento.
Uno de los problemas de este patrn es que no es recomendable si la estructura
de objetos cambia frecuentemente o es necesario aadir nuevos tipos de objetos
de forma habitual. Cada nuevo objeto que sea susceptible de ser visitado puede
provocar grandes cambios en la jerarqua de los visitantes.
[150]
Al igual que los patrones, los idioms tienen un nombre asociado (adems de sus alias),
la definicin del problema que resuelven y bajo qu contexto, as como algunas consideraciones (eficiencia, etc.). A continuacin, se muestran algunos de los ms relevantes. En
la seccin de Patrones de Diseo Avanzados se exploran ms de ellos.
#include <vector>
struct A {
A() : a(new char[3000]) {}
~A() { delete [] a; }
char* a;
};
int main() {
A var;
std::vector<A> v;
v.push_back(var);
return 0;
}
Qu es lo que ha pasado? No estamos reservando memoria en el constructor y liberando en el destructor? Cmo es posible que haya corrupcin de memoria? La solucin
al enigma es lo que no se ve en el cdigo. Si no lo define el usuario el compilador de C++
aade automticamente un constructor de copia que implementa la estrategia ms simple,
copia de todos los miembros. En particular cuando llamamos a push_back() creamos una
copia de var. Esa copia recibe a su vez una copia del miembro var.a que es un puntero
a memoria ya reservada. Cuando se termina main() se llama al destructor de var y del
vector. Al destruir el vector se destruyen todos los elementos. En particular se destruye
la copia de var, que a su vez libera la memoria apuntada por su miembro a, que apunta a
la misma memoria que ya haba liberado el destructor de var.
Antes de avanzar ms en esta seccin conviene formalizar un poco la estructura que
debe tener una clase en C++ para no tener sorpresas. Bsicamente se trata de especificar
todo lo que debe implementar una clase para poder ser usada como un tipo cualquiera:
Pasarlo como parmetro por valor o como resultado de una funcin.
Crear arrays y contenedores de la STL.
Usar algoritmos de la STL sobre estos contenedores.
Para que no aparezcan sorpresas una clase no trivial debe tener como mnimo:
1. Constructor por defecto. Sin l sera imposible instanciar arrays y no funcionaran
los contenedores de la STL.
[151]
2. Constructor de copia. Sin l no podramos pasar argumentos por valor, ni devolverlo como resultado de una funcin.
3. Operador de asignacin. Sin l no funcionara la mayora de los algoritmos sobre
contenedores.
4. Destructor. Es necesario para liberar la memoria dinmica reservada. El destructor
por defecto puede valer si no hay reserva explcita.
A este conjunto de reglas se le llama normalmente forma cannica ortodoxa (orthodox
canonical form).
Adems, si la clase tiene alguna funcin virtual, el destructor debe ser virtual. Esto es
as porque si alguna funcin virtual es sobrecargada en clases derivadas podran reservar
memoria dinmica que habra que liberar en el destructor. Si el destructor no es virtual
no se podra garantizar que se llama. Por ejemplo, porque la instancia est siendo usada a
travs de un puntero a la clase base.
[152]
class Vehicle
public:
virtual ~Vehicle();
6
7
};
8
9
10
11
public:
12
virtual ~Car();
13
14
15
};
16
17
18
19
public:
20
virtual ~Motorbike();
21
22
23
};
class Final
~Final() {} // privado
friend class A;
};
6
7
{ };
9
10
class B : public B
11
{ };
12
13
14
{
B b; // fallo de compilacin
15
16
4.6.4. pImpl
Pointer To Implementation (pImpl), tambin conocido como Handle Body u Opaque
Pointer, es un famoso idiom (utilizado en otros muchos) para ocultar la implementacin
de una clase en C++. Este mecanismo puede ser muy til sobre todo cuando se tienen
componentes reutilizables cuya declaracin o interfaz puede cambiar, lo cual implicara
que todos sus usuarios deberan recompilar. El objetivo es minimizar el impacto de un
cambio en la declaracin de la clase a sus usuarios.
[153]
En C++, un cambio en las variables miembro de una clase o en los mtodos inline
puede suponer que los usuarios de dicha clase tengan que recompilar. Para resolverlo,
la idea de pImpl es que la clase ofrezca una interfaz pblica bien definida y que sta
contenga un puntero a su implementacin, descrita de forma privada.
Por ejemplo, la clase Vehicle podra ser de la siguiente forma:
Listado 4.18: Ejemplo bsico de clase Vehicle
1
2
3
4
5
6
7
8
class Vehicle
{
public:
void run(int distance);
private:
int _wheels;
};
Como se puede ver, es una clase muy sencilla ya que ofrece slo un mtodo pblico.
Sin embargo, si queremos modificarla aadiendo ms atributos o nuevos mtodos privados, se obligar a los usuarios de la clase a recompilar por algo que realmente no utilizan
directamente. Usando pImpl, quedara el siguiente esquema:
Listado 4.19: Clase Vehicle usando pImpl (Vehicle.h)
1
2
3
4
5
6
7
8
9
10
11
/* Vehicle.cpp */
#include <Vehicle.h>
Vehicle::Vehicle()
{
_pimpl = new VehicleImpl();
}
void Vehicle::run()
{
_pimpl->run();
}
[154]
/* VehicleImpl.h */
class VehicleImpl
{
public:
void run();
private:
int _wheels;
std::string name;
};
Si la clase Vehicle est bien definida, se puede cambiar su implementacin sin obligar
a sus usuarios a recompilar. Esto proporciona una mayor flexibilidad a la hora de definir,
implementar y realizar cambios sobre la clase VehicleImpl.
[156]
<type>
it = begin ()
<type>
++it
<type>
<type>
...
end ()
[157]
Iteradores
<iterator>
Algoritmos
<algorithm>
<cstdlib>
algoritmos generales
bsearch() y qsort()
Cadenas
<string>
<cctype>
<cwctype>
<cstring>
<cwchar>
<cstdlib>
cadena
clasificacin de caracteres
clasificacin caracteres extendidos
funciones de cadena
funciones caracteres extendidos*
funciones cadena*
Entrada/Salida
<iosfwd>
<iostream>
<ios>
<streambuf>
<istream>
<ostream>
<iomanip>
<sstream>
<cstdlib>
<fstream>
<cstdio>
<cwchar>
lmites numricos
macros lmites numricos escalares*
macros lmites numricos pto flotante*
gestin de memoria dinmica
soporte a identificacin de tipos
soporte al tratamiento de excepciones
soporte de la biblioteca al lenguaje C
lista parm. funcin long. variable
rebobinado de la pila*
finalizacin del programa
reloj del sistema
tratamiento de seales*
Contenedores
<vector>
<list>
<deque>
<queue>
<stack>
<map>
<set>
<bitset>
array unidimensional
lista doblemente enlazada
cola de doble extremo
cola
pila
array asociativo
conjunto
array de booleanos
Diagnsticos
<exception>
<stdexcept>
<cassert>
<cerrno>
clase excepcin
excepciones estndar
macro assert
tratamiento errores*
Utilidades generales
<utility>
<functional>
<memory>
<ctime>
operadores y pares
objetos funcin
asignadores contenedores
fecha y hora*
Nmeros
<complex>
<valarray>
<numeric>
<cmath>
<cstdlib>
Figura 5.2: Visin general de la organizacin de STL [17]. El asterisco referencia a elementos con el estilo del
lenguaje C.
[158]
#include <iostream>
#include <vector>
using namespace std;
int main () {
vector<int> v;
v.push_back(7);
v.push_back(4);
v.push_back(6);
vector<int>::iterator it;
for (it = v.begin(); //
it != v.end(); //
++it)
//
cout << *it << endl;
// Declarar el iterador.
return 0;
}
5.2.
[159]
5.2.2. Rendimiento
Uno de los aspectos crticos en el mbito del desarrollo de videojuegos es el rendimiento, ya que es fundamental para lograr un sensacin de interactividad y para dotar
de sensacin de realismo el usuario de videojuegos. Recuerde que un videojuego es una
aplicacin grfica 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 aplicacin es crtico 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 aos, considerando las propiedades intrnsecas de cada uno de sus elementos y de las plataformas
sobre las que se ejecutar.
[160]
5.2.3. Inconvenientes
El uso de STL tambin puede presentar ciertos inconvenientes; algunos de ellos directamente vinculados a su propia complejidad [3]. En este contexto, uno de los principales
inconvenientes al utilizar STL es la depuracin de programas, debido a aspectos como
el uso extensivo de plantillas por parte de STL. Este planteamiento complica bastante la
inclusin de puntos de ruptura y la depuracin interactiva. Sin embargo, este problema no
es tan relevante si se supone que STL ha sido extensamente probado y depurado, por lo
que en teora no sera necesario llegar a dicho nivel.
Por otra parte, visualizar de manera directa el contenido de los contenedores o estructuras de datos utilizadas puede resultar complejo. A veces, es muy complicado conocer
exactamente a qu elemento est apuntando un iterador o simplemente ver todos los elementos de un vector.
La ltima desventaja es la asignacin de memoria, debido a que se pretende que
STL se utilice en cualquier entorno de cmputo de propsito general. En este contexto, es
lgico suponer que dicho entorno tenga suficiente memoria y la penalizacin por asignar
memoria no es crtica. Aunque esta situacin es la ms comn a la hora de plantear un
desarrollo, en determinados casos esta suposicin no se puede aplicar, como por ejemplo
en el desarrollo de juegos para consolas de sobremesa. Note que este tipo de plataformas
suelen tener restricciones de memoria, por lo que es crtico controlar este factor para
obtener un buen rendimiento.
1 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2271.html
5.3. Secuencias
[161]
Una posible solucin a este inconveniente consiste en utilizar asignadores de memoria personalizados cuando se utilizan contenedores especficos, de manera que el desarrollador puede controlar cmo y cundo se asigna memoria. Obviamente, esta solucin
implica que dicho desarrollador tenga un nivel de especializacin considerable, ya que
esta tarea no es trivial. En trminos general, el uso de asignadores personalizados se lleva
a cabo cuando el desarrollo de un videojuego se encuentra en un estado avanzado.
En general, hacer uso de STL para desarrollar un videojuego es una de las mejores alternativas posibles. En el mbito comercial, un gran nmero de juegos de ordenador
y de consola han hecho uso de STL. Recuerde que siempre es posible personalizar
algunos aspectos de STL, como la asignacin de memoria, en funcin de las restricciones existentes.
5.3.
Secuencias
La principal caracterstica de los contenedores de secuencia es que los elementos almacenados mantienen un orden determinado. La insercin y eliminacin de elementos
se puede realizar en cualquier posicin debido a que los elementos residen en una secuencia concreta. A continuacin se lleva a cabo una discusin de tres de los contenedores de
secuencia ms utilizados: el vector (vector), la cola de doble fin (deque) y la lista (list).
5.3.1. Vector
El vector es uno de los contenedores ms simples y
ms utilizados de STL, ya que posibilita la insercin y
eliminacin de elementos en cualquier posicin. Sin embargo, la complejidad computacional de dichas operaciones depende de la posicin exacta en la que se inserta o
elimina, respectivamente, el elemento en cuestin. Dicha
complejidad determina el rendimiento de la operacin 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 elemen- Figura 5.4: Los contenedores de secuencia mantienen un orden determito, de manera similar al acceso en los arrays de C.
nado a la hora de almacenar elemenA diferencia de lo que ocurre con los arrays, un vector tos, posibilitando optimizar el acceso
no tiene lmite en cuanto al nmero de elementos que se y gestionando la consistencia cach.
pueden aadir. Al menos, mientras el sistema tenga memoria disponible. En caso de utilizar un array, el programador ha de comprobar continuamente el tamao del mismo para asegurarse de que la inserccin es factible, evitando as
potenciales violaciones de segmento. En este contexto, el vector representa una solucin
a este tipo de problemas.
Los vectores proporcionan operaciones para aadir y eliminar elementos al final, insertar y eliminar elementos en cualquier posicin y acceder a los mismos en tiempo constante a partir de un ndice2 .
2 http://www.cplusplus.com/reference/stl/vector/
[162]
Implementacin
Cabecera
#elementos
Tamao elemento
Inicio
Elemento 1
Tpicamente, el contenido de los vectores se almacena de manera contigua en un bloque de memoria, el cual
suele contemplar el uso de espacio adicional para futuros
elementos. Cada elemento de un vector se almacena utilizando un desplazamiento a la derecha, sin necesidad de
hacer uso de punteros extra. De este modo, y partiendo de
la base de que todos los elementos del vector ocupan el
mismo tamao debido a que son del mismo tipo, la localizacin de los mismos se calcula a partir del ndice en la
secuencia y del propio tamao del elemento.
Los vectores utilizan una pequea cabecera que contiene informacin general del contenedor, como el puntero al inicio del mismo, el nmero actual de elementos
y el tamao actual del bloque de memoria reservado (ver
figura 5.5).
Elemento 2
Elemento 3
Rendimiento
vector<int>::iterator it;
for (it = _vector.begin(); it != _vector.end(); ++it) {
int valor = *it;
// Utilizar valor...
}
5.3. Secuencias
[163]
Respecto al uso de memoria, los vectores gestionan sus elementos en un gran bloque de memoria
que permita almacenar los elementos actualmente contenidos y considere algn espacio extra para elementos futuros. Si el bloque inicial no puede albergar un nuevo elemento, entonces el vector
reserva un bloque de memoria mayor, comnmente con el doble de tamao, copia el contenido del
bloque inicial al nuevo y libera el bloque de memoria inicial. Este planteamiento evita que el vector tenga que reasignar memoria con frecuencia. En
este contexto, es importante resaltar que un vector
no garantiza la validez de un puntero o un iterador
despus de una insercin, ya que se podra dar esta situacin. Por lo tanto, si es necesario
acceder a un elemento en concreto, la solucin ideal pasa por utilizar el ndice junto con
el operador [].
Los vectores permiten la preasignacin de un nmero de entradas con el objetivo
de evitar la reasignacin de memoria y la copia de elementos a un nuevo bloque. Para
ello, se puede utilizar la operacin reserve de vector que permite especificar la cantidad
de memoria reservada para el vector y sus futuros elementos. La reserva de memoria
explcita puede contribuir a mejorar el rendimiento de los vectores y evitar un alto nmero
de operaciones de reserva.
Complejidad
La nomenclatura O se suele utilizar
para acotar superiormente la complejidad de una determinada funcin o
algoritmo. Los rdenes de complejidad ms utilizados son O(1) (complejidad constante), O(logn) (complejidad logartmica), O(n) (complejidad
lineal), O(nlogn), O(n2 ) (complejidad cuadrtica), O(n3 ) (complejidad
cbica), O(nx ) (complejidad polinomial), O(bn ) (complejidad exponencial).
#include <iostream>
#include <vector>
3
4
5
6
int main () {
vector<int> v;
7
8
v.reserve(4);
10
12
v.push_back(7); v.push_back(6);
v.push_back(4); v.push_back(6);
13
11
14
15
16
17
18
return 0;
19
20
El caso contrario a esta situacin est representado por vectores que contienen datos que se usan durante un clculo en concreto pero que despus se descartan. Si esta
situacin se repite de manera continuada, entonces se est asignando y liberando el vector constantemente. Una posible solucin consiste en mantener el vector como esttico y
limpiar todos los elementos despus de utilizarlos. Este planteamiento hace que el vector
quede vaco y se llame al destructor de cada uno de los elementos previamente contenidos.
[164]
5.3.2. Deque
Deque proviene del trmino ingls double-ended queue y representa una cola de doble
fin. Este tipo de contenedor es muy parecido al vector, ya que proporciona acceso directo
a cualquier elemento y permite la insercin y eliminacin en cualquier posicin, aunque
con distinto impacto en el rendimiento. La principal diferencia reside en que tanto la
insercin como eliminacin del primer y ltimo elemento de la cola son muy rpidas. En
concreto, tienen una complejidad constante, es decir, O(1).
La colas de doble fin incluyen la funcionalidad bsica de los vectores pero tambin
considera operaciones para insertar y eliminar elementos, de manera explcita, al principio
del contenedor3 . Sin embargo, la cola de doble fin no garantiza que todos los elementos
almacenados residan en direcciones contiguas de memoria. Por lo tanto, no es posible
realizar un acceso seguro a los mismos mediante aritmtica de punteros.
Implementacin
Este contenedor de secuencia, a diferencia de los vectores, mantiene varios bloques
de memoria, en lugar de uno solo, de forma que se reservan nuevos bloques conforme
el contenedor va creciendo en tamao. Al contrario que ocurra con los vectores, no es
necesario hacer copias de datos cuando se reserva un nuevo bloque de memoria, ya que
los reservados anteriormente siguen siendo utilizados.
3 http://www.cplusplus.com/reference/stl/deque/
5.3. Secuencias
Cabecera
[165]
Bloque 1
Principio
Vaco
Final
Elemento 1
#elementos
Elemento 2
Tamao elemento
Bloque 1
Bloque 2
Bloque 2
Elemento 3
Bloque n
Elemento 4
Elemento 5
Vaco
Bloque n
Elemento 6
Elemento j
Elemento k
Vaco
La cabecera de la cola de doble fin almacena una serie de punteros a cada uno de los
bloques de memoria, por lo que su tamao aumentar si se aaden nuevos bloques que
contengan nuevos elementos.
Rendimiento
Al igual que ocurre con los vectores, la insercin y eliminacin de elementos al final
de la cola implica una complejidad constante, mientras que en una posicin aleatoria
en el centro del contenedor implica una complejidad lineal. Sin embargo, a diferencia
del vector, la insercin y eliminacin de elementos al principio es tan rpida como al
final. La consecuencia directa es que las colas de doble fin son el candidato perfecto para
estructuras FIFO (First In, First Out). En el mbito del desarrollo de videojuegos, las
estructuras FIFO se suelen utilizar para modelar colas de mensajes o colas con prioridad
para atender peticiones asociadas a distintas tareas.
El hecho de manejar distintos bloques de memoria hace que el rendimiento se degrade
mnimamente cuando se accede a un elemento que est en otro bloque distinto. Note que
dentro de un mismo bloque, los elementos se almacenan de manera secuencial al igual
que en los vectores. Sin embargo, el recorrido transversal de una cola de doble fin es
prcticamente igual de rpido que en el caso de un vector.
[166]
5.3.3. List
La lista es otro contenedor de secuencia que difiere de los dos anteriores. En primer
lugar, la lista proporciona iteradores bidireccionales, es decir, iteradores que permiten
navegar en los dos sentidos posibles: hacia adelante y hacia atrs. En segundo lugar, la
lista no proporciona un acceso aleatorio a los elementos que contiene, como s hacen tanto
los vectores como las colas de doble fin. Por lo tanto, cualquier algoritmo que haga uso
de este tipo de acceso no se puede aplicar directamente sobre listas.
La principal ventaja de la lista es el rendimiento
y la conveniencia para determinadas operaciones,
suponiendo que se dispone del iterador necesario
para apuntar a una determinada posicin.
La especificacin de listas de STL ofrece una
funcionalidad bsica muy similar a la de cola de
doble fin, pero tambin incluye funcionalidad relativa a aspectos ms complejos como la
fusin de listas o la bsqueda de elementos4 .
Algoritmos en listas
La propia naturaleza de la lista permite
que se pueda utilizar con un buen rendimiento para implementar algoritmos
o utilizar los ya existentes en la propia
biblioteca.
Implementacin
Este contenedor se implementa mediante una lista doblemente enlazada de elementos. Al contrario del planteamiento de los dos contenedores previamente discutidos, la
memoria asociada a los elementos de la lista se reserva individualmente, de manera que
cada nodo contiene un puntero al anterior elemento y otro al siguiente (ver figura 5.7).
La lista tambin tiene vinculada una cabecera con informacin relevante, destacando
especialmente los punteros al primer y ltimo elemento de la lista.
4 http://www.cplusplus.com/reference/stl/list/
5.3. Secuencias
[167]
Cabecera
Principio
Siguiente
Siguiente
Siguiente
Final
Anterior
Anterior
Anterior
Elemento 1
Elemento 2
Elemento 3
#elementos
Tamao elemento
...
Rendimiento
La principal ventaja de una lista en trminos de rendimiento es que la insercin y
eliminacin de elementos en cualquier posicin se realiza en tiempo constante, es decir,
O(1). Sin embargo, para garantizar esta propiedad es necesario obtener un iterador a la
posicin deseada, operacin que puede tener una complejidad no constante.
Listado 5.4: Uso bsico de listas con STL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <list>
#include <stdlib.h>
using namespace std;
class Clase {
public:
Clase (int id, int num_alumnos):
_id(id), _num_alumnos(num_alumnos) {}
int getId () const { return _id; }
int getNumAlumnos () const { return _num_alumnos;
bool operator< (const Clase &c) const { // Sobrecarga del operador para poder comparar clases.
return (_num_alumnos < c.getNumAlumnos());
}
private:
int _id;
int _num_alumnos;
};
void muestra_clases (list<Clase> lista);
int main () {
list<Clase> clases; // Lista de clases.
srand(time(NULL));
for (int i = 0; i < 7; ++i) // Insercin de clases.
clases.push_back(Clase(i, int(rand() % 30 + 10)));
muestra_clases(clases);
// Se ordena la lista de clases (usa la implementacin del operador de sobrecarga)
clases.sort();
muestra_clases(clases);
return 0;
}
[168]
Una desventaja con respecto a los vectores y a las colas de doble fin es que el recorrido transvesal de las listas es mucho ms lento, debido a que es necesario leer un
puntero para acceder a cada nodo y, adems, los nodos se encuentran en fragmentos de
memoria no adyacentes. Desde un punto de vista general, el recorrido de elementos en
una lista puede ser de hasta un orden de magnitud ms lento que en vectores. Adems,
las operaciones que implican el movimiento de bloques de elementos en listas se realiza
en tiempo constante, incluso cuando dichas operaciones se realizan utilizando ms de un
contenedor.
El planteamiento basado en manejar punteros hace que las listas sean ms eficientes
a la hora de, por ejemplo, reordenar sus elementos, ya que no es necesario realizar copias
de datos. En su lugar, simplemente hay que modificar el valor de los punteros de acuerdo
al resultado esperado o planteado por un determinado algoritmo. En este contexto, a mayor nmero de elementos en una lista, mayor beneficio en comparacin con otro tipo de
contenedores.
El anterior listado de cdigo mostrar el siguiente resultado por la salida estndar:
0: 23 1: 28 2: 10 3: 17 4: 20 5: 22 6: 11
2: 10 6: 11 3: 17 4: 20 5: 22 0: 23 1: 28
el operador < (lneas 14-16 ). Internamente, la lista de STL hace uso de esta informacin
para ordenar los elementos y, desde un punto de vista general, se basa en la sobrecarga de
dicho operador para ordenar, entre otras funciones, tipos de datos definidos por el propio
usuario.
Respecto al uso en memoria, el planteamiento de la lista reside en reservar pequeos
bloques de memoria cuando se inserta un nuevo elemento en el contenedor. La principal
ventaja de este enfoque es que no es necesario reasignar datos. Adems, los punteros e
iteradores a los elementos se preservan cuando se realizan inserciones y eliminaciones,
posibilitando la implementacin de distintas clases de algoritmos.
Evidentemente, la principal desventaja es que
casi cualquier operacin implica una nueva asignacin de memoria. Este inconveniente se puede
solventar utilizando asignadores de memoria personalizados, los cuales pueden usarse incluso para
mejorar la penalizacin que implica tener los datos
asociados a los nodos de la lista en distintas partes
de la memoria.
En el mbito del desarrollo de videojuegos, especialmente en plataformas con restricciones de memoria, las listas pueden degradar el
rendimiento debido a que cada nodo implica una cantidad extra, aunque pequea, de memoria para llevar a cabo su gestin.
Asignacin de memoria
El desarrollo de juegos y la gestin de
servidores de juego con altas cargas
computacionales implica la implementacin asignadores de memoria personalizados. Este planteamiento permite
mejorar el rendimiento de alternativas
clsicas basadas en el uso de stacks y
heaps.
[169]
Vector
O(1)
O(n)
O(n)
Rpido
Raramente
S
Tras I/E
Deque
O(1)
O(1)
O(n)
Rpido
Peridicamente
Casi siempre
Tras I/E
List
O(1)
O(1)
O(1)
Menos rpido
Con cada I/E
No
Nunca
Tabla 5.1: Resumen de las principales propiedades de los contenedores de secuencia previamente estudiados
(I/E = insercin/eliminacin).
A continuacin se listan algunos de los posibles usos de listas en el mbito del desarrollo de videojuegos:
Lista de entidades del juego, suponiendo un alto nmero de inserciones y eliminaciones.
Lista de mallas a renderizar en un frame en particular, suponiendo una ordenacin
en base a algn criterio como el material o el estado. En general, se puede evaluar
el uso de listas cuando sea necesario aplicar algn criterio de ordenacin.
Lista dinmica de posibles objetivos a evaluar por un componente de Inteligencia
Artificial, suponiendo un alto nmero de inserciones y eliminaciones.
5.4.
Contenedores asociativos
[170]
para poder pertenecer al mismo. Note cmo la comparacin se realiza en las lneas 9-11
con el operador <, es decir, mediante la funcin menor. Este enfoque es bastante comn
en STL, en lugar de utilizar el operador de igualdad.
Listado 5.5: Uso bsico de conjuntos con STL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
#define DISTANCIA 5
struct ValorAbsMenos {
bool operator() (const int& v1, const int& v2) const {
return (abs(v1 - v2) < DISTANCIA);
}
};
void recorrer (set<int, ValorAbsMenos> valores);
int main () {
set<int, ValorAbsMenos> valores;
valores.insert(5);
valores.insert(3);
valores.insert(9);
valores.insert(7);
recorrer(valores);
return 0;
}
5 http://www.cplusplus.com/reference/stl/set/
6 http://www.cplusplus.com/reference/stl/multiset/
[171]
Cabecera
Principio
Final
Hijoi
#elementos
Elemento
Hijod
Tamao elemento
...
Hijoi
Hijoi
Elemento
Hijodi
Elemento
Hijodi
Hijoi
Hijodi
Elemento
Elemento
Hijod
Hijodi
[172]
Una alternativa posible para buscar elementos de manera eficiente puede ser un vector,
planteando para ello una ordenacin de elementos seguida de una bsqueda binaria. Este
planteamiento puede resultar eficiente si el nmero de bsquedas es muy reducido y el
nmero de accesos es elevado.
La insercin de elementos en un rbol balanceado tiene una complejidad logartmica,
pero puede implicar la reordenacin de los mismos para mantener balanceada la estructura. Por otra parte, el recorrido de los elementos de un conjunto es muy similar al de listas,
es decir, se basa en el uso de manejo de enlaces a los siguientes nodos de la estructura.
Respecto al uso de memoria, la implementacin basada en rboles binarios balanceados puede no ser la mejor opcin ya que la insercin de elementos implica la asignacin
de un nuevo nodo, mientras que la eliminacin implica la liberacin del mismo.
Cundo usar sets y multisets?
En general, este tipo de contenedores asociativos estn ideados para situaciones especficas, como por ejemplo el hecho de manejar conjuntos con elementos no redundantes
o cuando el nmero de bsquedas es relevante para obtener un rendimiento aceptable. En
el mbito del desarrollo de videojuegos, este tipo de contenedores se pueden utilizar para
mantener un conjunto de objetos de manera que sea posible cargar de manera automtica
actualizaciones de los mismos, permitiendo su sobreescritura en funcin de determinados
criterios.
[173]
Tanto el map7 como el multimap8 son contenedores asociativos ordenados, por lo que
es posible recorrer su contenido en el orden en el que estn almacenados. Sin embargo,
este orden no es el orden de insercin inicial, como ocurre con los conjuntos, sino que
viene determinado por aplicar una funcin de comparacin.
Implementacin
La implementacin tpica de este tipo de contenedores es idntica a la de los conjuntos, es decir, mediante un rbol binario balanceado. Sin embargo, la diferencia reside
en que las claves para acceder y ordenar los elementos son objetos distintos a los propios
elementos.
Rendimiento
El rendimiento de estos contenedores es prcticamente idntico al de los conjuntos,
salvo por el hecho de que las comparaciones se realizan a nivel de clave. De este modo,
es posible obtener cierta ventaja de manejar claves simples sobre una gran cantidad de
elementos complejos, mejorando el rendimiento de la aplicacin. Sin embargo, nunca
hay que olvidar que si se manejan claves ms complejas como cadenas o tipos de datos
definidos por el usuario, el rendimiento puede verse afectado negativamente.
Por otra parte, el uso del operador [] no tieEl operador []
ne el mismo rendimiento que en vectores, ya que
El operador [] se puede utilizar para acimplica realizar la bsqueda del elemento a partir
ceder, escribir o actualizar informacin
sobre algunos contenedores. Sin emde la clave y, por lo tanto, no sera de un orden de
bargo, es importante considerar el imcomplejidad constante sino logartmico. Al usar espacto en el rendimiento al utilizar dite operador, tambin hay que tener en cuenta que si
cho operador, el cual vendr determise escribe sobre un elemento que no existe, entonnado por el contenedor usado.
ces ste se aade al contenedor. Sin embargo, si se
intenta leer un elemento que no existe, entonces el resultado es que el elemento por defecto se aade para la clave en concreto y, al mismo tiempo, se devuelve dicho valor. El
siguiente listado de cdigo muestra un ejemplo.
Como se puede apreciar, el listado de cdigo muestra caractersticas propias de STL
para manejar los elementos del contenedor:
7 http://www.cplusplus.com/reference/stl/map/
8 http://www.cplusplus.com/reference/stl/multimap/
[174]
#include <iostream>
#include <map>
using namespace std;
int main () {
map<int, string> jugadores;
pair<map<int, string>::iterator, bool> ret;
// Insertar elementos.
jugadores.insert(pair<int, string>(1, "Luis"));
jugadores.insert(pair<int, string>(2, "Sergio"));
// Comprobando elementos ya insertados...
ret = jugadores.insert(pair<int, string>(2, "David"));
if (ret.second == false) {
cout << "El elemento 2 ya existe ";
cout << "con un valor de " << ret.first->second << endl;
}
jugadores[3] = "Alfredo"; // Insercin con []...
// Caso excepcional; se aade valor por defecto...
const string& j_aux = jugadores[4]; // jugadores[4] =
return 0;
}
Ante esta situacin, resulta deseable hacer uso de la operacin find para determinar si
un elemento pertenece o no al contenedor, accediendo al mismo con el iterador devuelto
por dicha operacin. As mismo, la insercin de elementos es ms eficiente con insert
en lugar de con el operador [], debido a que este ltimo implica un mayor nmero de
copias del elemento a insertar. Por el contrario, [] es ligeramente ms eficiente a la hora
de actualizar los contenidos del contenedor en lugar de insert.
Respecto al uso de memoria, los mapas tienen un comportamiento idntico al de los
conjuntos, por lo que se puede suponer los mismos criterios de aplicacin.
Usando referencias. Recuerde consultar informacin sobre las operaciones de cada
uno de los contenedores estudiados para conocer exactamente su signatura y cmo
invocarlas de manera eficiente.
[175]
Set
O(logn)
O(logn)
Ms lento que lista
Con cada I/E
No
Nunca
Map
O(logn)
O(logn)
Ms lento que lista
Con cada I/E
No
Nunca
Tabla 5.2: Resumen de las principales propiedades de los contenedores asociativos previamente estudiados (I/E
= insercin/eliminacin).
5.5.
Adaptadores de secuencia
Cima
Elemento 1
Elemento 2
Elemento 3
...
Elemento i
...
Elemento n
[176]
5.5.1. Stack
El adaptador ms sencillo en STL es la pila o stack9 . Las principales operaciones sobre
una pila son la insercin y eliminacin de elementos por uno de sus extremos: la cima. En
la literatura, estas operaciones se conocen como push y pop, respectivamente.
La pila es ms restrictiva en trminos funcionales que los distintos contenedores de
secuencia previamente estudiados y, por lo tanto, se puede implementar con cualquiera
de ellos. Obviamente, el rendimiento de las distintas versiones vendr determinado por el
contenedor elegido. Por defecto, la pila se implementa utilizando una cola de doble fin.
El siguiente listado de cdigo muestra cmo hacer uso de algunas de las operaciones
ms relevantes de la pila para invertir el contenido de un vector.
Listado 5.7: Inversin del contenido de un vector con una pila
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int main () {
vector<int> fichas;
vector<int>::iterator it;
stack<int> pila;
for (int i = 0; i < 10; i++) // Rellenar el vector
fichas.push_back(i);
for (it = fichas.begin(); it != fichas.end(); ++it)
pila.push(*it); // Apilar elementos para invertir
fichas.clear(); // Limpiar el vector
while (!pila.empty()) { // Rellenar el vector
fichas.push_back(pila.top());
pila.pop();
}
return 0;
}
5.5.2. Queue
Al igual que ocurre con la pila, la cola o queue10 es otra estructura de datos de uso muy
comn que est representada en STL mediante un adaptador de secuencia. Bsicamente, la
cola mantiene una interfaz que permite la insercin de elementos al final y la extraccin
de elementos slo por el principio. En este contexto, no es posible acceder, insertar y
eliminar elementos que estn en otra posicin.
9 http://www.cplusplus.com/reference/stl/stack/
10 http://www.cplusplus.com/reference/stl/queue/
[177]
Insercin
Elemento 1
Elemento 2
...
dad o priority queue11 como caso especial de cola previamente discutido. La diferencia entre los dos adaptadores
Elemento i
reside en que el elemento listo para ser eliminado de la
cola es aqul que tiene una mayor prioridad, no el elemento que se aadi en primer lugar.
...
As mismo, la cola con prioridad incluye cierta funcionalidad especfica, a diferencia del resto de adaptadoElemento n
res estudiados. Bsicamente, cuando un elemento se aade a la cola, entonces ste se ordena de acuerdo a una
funcin de prioridad.
Por defecto, la cola con prioridad se apoya en la implementacin de vector, pero es posible utilizarla tambin
con deque. Sin embargo, no se puede utilizar una lista deExtraccin
bido a que la cola con prioridad requiere un acceso aleatorio para insertar eficientemente elementos ordenados.
Figura 5.12: Visin abstracta de una
o queue. Los elementos solaLa comparacin de elementos sigue el mismo esque- cola
mente se aaden por el final y se elima que el estudiado en la seccin 5.4.1 y que permita minan por el principio.
definir el criterio de inclusin de elementos en un conjunto, el cual estaba basado en el uso del operador menor
que.
La cola de prioridad se puede utilizar para mltiples
aspectos en el desarrollo de videojuegos, como por ejemplo la posibilidad de mantener una estructura ordenada por prioridad con aquellos objetos
que estn ms cercanos a la posicin de la cmara virtual. As, este contenedor ofrece
mayor flexibilidad para determinadas situaciones en las que son necesarias esquemas basados en la importancio de los elementos a gestionar.
11 http://www.cplusplus.com/reference/stl/priority_queue/
[180]
Esta tcnica es en realidad muy parecida a la que se plantea en el desarrollo de interfaces grficas de usuario (GUI (Graphical User Interface)), donde gran parte de las mismas
es esttica y slo 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.
Dicha tcnica se suele denominar rectangle invalidation.
En el desarrollo de videojuegos 3D, aunque manteniendo la idea de dibujar el mnimo nmero de primitivas
necesarias en cada iteracin del bucle de renderizado, la
filosofa es radicalmente distinta. En general, al mismo
tiempo que la cmara se mueve en el espacio tridimensional, el contenido audiovisual cambia continuamente, por
lo que no es viable aplicar tcnicas tan simples como la
mencionada anteriormente.
La consecuencia directa de este esquema es la
Figura 6.1: El bucle de juego repre- necesidad de un bucle de renderizado que muessenta la estructura de control princi- tre los distintas imgenes o frames percibidas por
pal de cualquier juego y gobierna su
funcionamiento y la transicin entre la cmara virtual con una velocidad lo suficientelos distintos estados del mismo.
mente elevada para transmitir una sensacin de realidad.
El siguiente listado de cdigo muestra la estructura general de un bucle de renderizado.
Listado 6.1: Esquema general de un bucle de renderizado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (true) {
// Actualizar la cmara,
// normalmente de acuerdo a un camino prefijado.
update_camera ();
// Actualizar la posicin, orientacin y
// resto de estado de las entidades del juego.
update_scene_entities ();
// Renderizar un frame en el buffer trasero.
render_scene ();
// Intercambiar el contenido del buffer trasero
// con el que se utilizar para actualizar el
// dispositivo de visualizacin.
swap_buffers ();
}
[181]
La mayora de estos componentes han de actualizarse peridicamente mientras el juego se encuentra en ejecucin. Por ejemplo, el sistema de animacin, de manera sincronizada con respecto al motor de renderizado, ha de actualizarse con una frecuencia de 30
60 Hz con el objetivo de obtener una tasa de frames por segundo lo suficientemente
elevada para garantizar una sensacin de realismo adecuada. Sin embargo, no es necesario mantener este nivel de exigencia para otros componentes, como por ejemplo el de
Inteligencia Artificial.
De cualquier modo, es necesario un planteamiento que permita actualizar el estado
de cada uno de los subsistemas y que considere las restricciones temporales de los mismos. Tpicamente, este planteamiento se suele abordar mediante el bucle de juego, cuya
principal responsabilidad consiste en actualizar el estado de los distintos componentes
del motor tanto desde el punto de vista interno (ej. coordinacin entre subsistemas) como
desde el punto de vista externo (ej. tratamiento de eventos de teclado o ratn).
Listado 6.2: Esquema general del bucle de juego.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
move_paddles();
// Actualizar palas.
move_ball();
// Actualizar bola.
collision_detection(); // Tratamiento de colisiones.
// Anot algn jugador?
if (ballReachedBorder(LEFT_PLAYER)) {
score(RIGHT_PLAYER);
reset_ball();
}
if (ballReachedBorder(RIGHT_PLAYER)) {
score(LEFT_PLAYER);
reset_ball();
}
render();
// Renderizado.
}
}
[182]
La arquitectura del bucle de juego se puede implementar de diferentes formas mediante distintos planteamientos. Sin embargo, la mayora de ellos tienen en comn el uso de uno o varios bucles de control que gobiernan la actualizacin e interaccin con los distintos componentes del motor de juegos. En esta seccin se realiza un breve recorrido por las alternativas ms populares,
resaltando especialmente un planteamiento basado en la
gestin 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.
message
dispatching
Sistema
operativo
[183]
#include <GL/glut.h>
#include <GL/glu.h>
#include <GL/gl.h>
// Se omite parte del cdigo fuente...
void update (unsigned char key, int x, int y) {
Rearthyear += 0.2;
Rearthday += 5.8;
glutPostRedisplay();
}
int main (int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
glutInitWindowSize(640, 480);
glutCreateWindow("Session #04 - Solar System");
// Definicin de las funciones de retrollamada.
glutDisplayFunc(display);
glutReshapeFunc(resize);
// Eg. update se ejecutar cuando el sistema
// capture un evento de teclado.
// Signatura de glutKeyboardFunc:
// void glutKeyboardFunc(void (*func)
// (unsigned char key, int x, int y));
glutKeyboardFunc(update);
glutMainLoop();
return 0;
}
[184]
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 comn est representado por el jugador cuando
pulsa un botn del joystick, pero tambin se pueden identificar eventos a nivel interno,
como por ejemplo la reaparicin o respawn de un NPC en el juego.
Gran parte de los motores de juegos incluyen un subsistema especfico para el tratamiento de eventos, permitiendo al resto de componentes del motor o incluso a entidades
especficas registrarse como partes interesadas en un determinado tipo de eventos. Este
planteamiento est muy estrechamente relacionado con el patrn Observer.
El tratamiento de eventos es un aspecto transversal a otras arquitecturas diseadas para
tratar el bucle de juego, por lo que es bastante comn integrarlo dentro de otros esquemas
ms generales, como por ejemplo el que se discute a continuacin y que est basado en la
gestin de distintos estados dentro del juego.
Canales de eventos. Con el objetivo de independizar los publicadores y los suscriptores de eventos, se suele utilizar el concepto de canal de eventos como mecanismo
de abstraccin.
Evidentemente, esta clasificacin es muy general ya que est planteada desde un punto de vista
muy abstracto. Por ejemplo, si consideramos aspectos ms especficos como por ejemplo el uso
de dispositivos como PlayStation MoveTM , WiimoteTM o KinectTM , sera necesario incluir un estado
de calibracin antes de poder utilizar estos dispositivos de manera satisfactoria.
Intro
[185]
Men
ppal
Juego
Game
over
Pausa
Figura 6.5: Visin general de una mquina de estados finita que representa los estados ms comunes en cualquier juego.
ogre3d.org/tikiwiki/Managing+Game+States+with+OGRE
http://www.
[186]
Ogre::
Singleton
InputManager
1
OIS::
Keyboard
OIS::
MouseListener
Ogre::
FrameListener
GameManager
OIS::
KeyListener
1..*
OIS::
MouseListener
Ogre::
Singleton
GameState
1
OIS::
Mouse
IntroState
PlayState
PauseState
Ogre::
Singleton
Figura 6.6: Diagrama de clases del esquema de gestin de estados de juego con Ogre3D. En un tono ms oscuro
se reflejan las clases especficas de dominio.
1. Gestin bsica del estado (lneas 19-22 ), para definir qu hacer cuando se entra,
sale, pausa o reanuda el estado.
3. Gestin bsica de eventos antes y despus del renderizado (lneas 37-38 ), operaciones tpicas de la clase Ogre::FrameListener.
Adicionalmente,
existe otro bloque de funciones relativas a la gestin bsica de tran
siciones (lneas 41-48 ), con operaciones para cambiar de estado, aadir un estado a la pila
de estados y volver a un estado anterior, respectivamente. Las transiciones implican una
interaccin con la entidad GameManager, que se discutir posteriormente.
La figura 6.6 muestra la relacin de la clase GameState con el resto de clases, as
como tres posibles especializaciones de la misma. Como se puede observar, esta clase
est relacionada con GameManager, responsable de la gestin de los distintos estados y
de sus transiciones.
Sin embargo, antes de pasar a discutir esta clase, en el diseo discutido se contempla
la definicin explcita de la clase InputManager, como punto central para la gestin de
eventos de entrada, como por ejemplo los de teclado o de ratn. La clase InputManager
implementa el patrn Singleton mediante las utilidades de Ogre3D. Es posible utilizar
otros esquemas para que su implementacin no depende de Ogre y se pueda utilizar con
otros frameworks.
El InputManager sirve como interfaz para las entidades que estn interesadas en procesar eventos de entrada (como se discutir ms adelante), ya que mantiene operaciones
para aadir y eliminar listeners de dos tipos: i) OIS::KeyListener y ii) OIS::MouseListener.
De hecho, esta clase hereda de ambas clases. Adems, implementa el patrn Singleton con
el objetivo de que slo exista una nica instancia de la misma.
[187]
#ifndef GameState_H
#define GameState_H
#include <Ogre.h>
#include <OIS/OIS.h>
#include "GameManager.h"
#include "InputManager.h"
// Clase abstracta de estado bsico.
// Definicin base sobre la que extender
// los estados del juego.
class GameState {
public:
GameState() {}
// Gestin bsica del estado.
virtual void enter () = 0;
virtual void exit () = 0;
virtual void pause () = 0;
virtual void resume () = 0;
// Gestin bsica para el tratamiento
// de eventos de teclado y ratn.
virtual void keyPressed (const OIS::KeyEvent &e) = 0;
virtual void keyReleased (const OIS::KeyEvent &e) = 0;
virtual void mouseMoved (const OIS::MouseEvent &e) = 0;
virtual void mousePressed (const OIS::MouseEvent &e,
OIS::MouseButtonID id) = 0;
virtual void mouseReleased (const OIS::MouseEvent &e,
OIS::MouseButtonID id) = 0;
// Gestin bsica para la gestin
// de eventos antes y despus de renderizar un frame.
virtual bool frameStarted (const Ogre::FrameEvent& evt) = 0;
virtual bool frameEnded (const Ogre::FrameEvent& evt) = 0;
// Gestin bsica de transiciones.
void changeState (GameState* state) {
GameManager::getSingletonPtr()->changeState(state);
}
void pushState (GameState* state) {
GameManager::getSingletonPtr()->pushState(state);
}
void popState () {
GameManager::getSingletonPtr()->popState();
}
};
#endif
[188]
Por otra parte, es importante destacar las variables miembro _keyboard y _mouse
que se utilizarn para capturar los eventos de teclado y ratn, respectivamente. Dicha
captura se realizar en cada frame mediante la funcin capture(), definida tanto en el
InputManager como en OIS::Keyboard y OIS::Mouse.
Listado 6.5: Clase InputManager.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[189]
Note que esta clase contiene una funcin miembro start() (lnea 12 ), definida de manera explcita para inicializar el gestor de juego, establecer el estado inicial (pasado como
parmetro) y arrancar el bucle de renderizado.
[190]
void
GameManager::start
(GameState* state)
{
// Creacin del objeto Ogre::Root.
_root = new Ogre::Root();
if (!configure())
return;
loadResources();
_inputMgr = new InputManager;
_inputMgr->initialise(_renderWindow);
// Registro como key y mouse listener...
_inputMgr->addKeyListener(this, "GameManager");
_inputMgr->addMouseListener(this, "GameManager");
// El GameManager es un FrameListener.
_root->addFrameListener(this);
// Transicin al estado inicial.
changeState(state);
// Bucle de rendering.
_root->startRendering();
}
PauseState
PlayState
IntroState
tecla 'p'
PauseState
PlayState
El listado de cdigo 6.8 muestra una posible implementacin de la funcin changeState() de la clase GameManager. Note cmo la estructura de pila de estados perFigura 6.7: Actualizacin de la pi- mite un acceso directo al estado actual (cima) para llevar
la de estados para reanudar el juego
a cabo las operaciones de gestin necesarias. Las transi(evento teclado p).
ciones se realizan con las tpicas operaciones de push y
pop.
IntroState
[191]
Otro aspecto relevante del diseo de esta clase es la delegacin de eventos de entrada
asociados a la interaccin por parte del usuario con el teclado y el ratn. El diseo discutido permite delegar directamente el tratamiento del evento al estado activo, es decir,
al estado que ocupe la cima de la pila. Del mismo modo, se traslada a dicho estado la
implementacin de las funciones frameStarted() y frameEnded(). El listado de cdigo 6.9
muestra cmo la implementacin de, por ejemplo, la funcin keyPressed() es trivial.
void
GameManager::changeState
(GameState* state)
{
// Limpieza del estado actual.
if (!_states.empty()) {
// exit() sobre el ltimo estado.
_states.top()->exit();
// Elimina el ltimo estado.
_states.pop();
}
// Transicin al nuevo estado.
_states.push(state);
// enter() sobre el nuevo estado.
_states.top()->enter();
}
bool
GameManager::keyPressed
(const OIS::KeyEvent &e)
{
_states.top()->keyPressed(e);
return true;
}
[192]
Figura 6.8: Capturas de pantalla del juego Supertux. Izquierda, estado de introduccin; medio, estado de juego;
derecha, estado de pausa.
As mismo, tambin especifca la funcionalidad vinculada a la gestin bsica de estados. Recuerde que, al extender la clase GameState, los estados especficos han de cumplir
el contrato funcional definido por dicha clase abstracta.
Finalmente, la transicin de estados se puede gestionar de diversas formas. Por ejemplo, es perfectamente posible realizarla mediante los eventos de teclado. En otras palabras,
la pulsacin de una tecla en un determinado estado sirve como disparador para pasar a
otro estado. Recuerde que el paso de un estado a otro puede implicar la ejecucin de las
funciones exit() o pause(), dependiendo de la transicin concreta.
[193]
en la lnea 16 ).
6.2.
En esta seccin se discute la gestin bsica de los recursos, haciendo especial hincapi
en dos casos de estudio concretos: i) la gestin bsica del sonido y ii) el sistema de
archivos. Antes de llevar a cabo un estudio especfico de estas dos cuestiones, la primera
seccin de este captulo introduce la problemtica de la gestin de recursos en los motores
de juegos. As mismo, se discuten las posibilidades que el framework Ogre3D ofrece para
gestionar dicha problemtica.
En el caso de la gestin bsica del sonido, el lector ser capaz de manejar una serie
de abstracciones, en torno a la biblioteca multimedia SDL, para integrar msica y efectos
de sonido en los juegos que desarrolle. Por otra parte, en la seccin relativa a la gestin del sistema de archivos se plantear la problemtica del tratamiento de archivos y se
estudiarn tcnicas de entrada/salida asncrona.
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 geometra
tridimensional, texturas, animaciones, sonidos, datos relativos a la gestin de fsica y
colisiones, etc. Evidentemente, esta naturaleza tan variada se ha de gestionar de forma
consistente y garantizando la integridad de los datos.
Por otra parte, las potenciales limitaciones hardware de la plataforma sobre la que se
ejecutar el juego implica que sea necesario plantear un mecanismo eficiente para cargar
y liberar los recursos asociados a dichos datos multimedia. Por lo tanto, una mxima del
motor de juegos es asegurarse que solamente existe una copia en memoria de un determinado recurso multimedia, de manera independiente al nmero de instancias que lo estn
utilizando en un determinado momento. Este esquema permite optimizar los recursos y
manejarlos de una forma adecuada.
Un ejemplo representativo de esta cuestin est representado por las mallas poligonales y las texturas. Si, por ejemplo, siete mallas comparten la misma textura, es decir,
la misma imagen bidimensional, entonces es deseable mantener una nica copia de la
textura en memoria principal, en lugar de mantener siete. En este contexto, la mayora
de motores de juegos integran algn tipo de gestor de recursos para cargar y gestionar
la diversidad de recursos que se utilizan en los juegos de ltima generacin. El gestor
de recursos o resource manager se suele denominar comnmente media manager o asset
manager.
[194]
Figura 6.9: Captura de pantalla del videojuego Lemmings 2. Nunca la gestin de recursos fue tan importante...
[195]
Ogre::
ResourceManager
NuevoGestor
0..*
Ogre::
SharedPtr
Ogre::
Resource
NuevoRecursoPtr
NuevoRecurso
Figura 6.10: Diagrama de clases de las principales clases utilizadas en Ogre3D para la gestin de recursos. En
un tono ms oscuro se reflejan las clases especficas de dominio.
Ogre::SharedPtr, clase que permite la gestin inteligente de recursos que necesitan una destruccin implcita.
Bsicamente, el desarrollador que haga uso del planteamiento que Ogre ofrece para
la gestin de recursos tendr que implementar algunas funciones heredadas de las clases
anteriormente mencionadas, completando as la implementacin especfica del dominio,
es decir, especfica de los recursos a gestionar. Ms adelante se discute en detalle un
ejemplo relativo a los recursos de sonido.
Una de las ideas fundamentales de la carga de recursos se basa en almacenar en
memoria principal una nica copia de cada recurso. ste se puede utilizar para representar o gestionar mltiples entidades.
[196]
SDL se puede integrar perfectamente con OpenGL para llevar a cabo la inicializacin
de la parte grfica de una aplicacin interactiva, delegando en SDL el propio tratamiento
de los eventos de entrada. Hasta ahora, en el mdulo 2 (Programacin Grfica) se haba
utilizado la biblioteca GLUT (OpenGL Utility Toolkit) para implementar programas que
hicieran uso de OpenGL. Sin embargo, SDL proporciona un gran nmero de ventajas con
respecto a esta alternativa2 :
SDL fue diseado para programadores de videojuegos.
SDL y Civilization. El port a sistemas GNU Linux del juego Civilization se realiz
haciendo uso de SDL. En este caso particular, los personajes se renderizaban haciendo uso de superficies.
$
$
$
$
sudo
sudo
sudo
sudo
apt-get
apt-get
apt-get
apt-get
update
install libsdl1.2-dev
install libsdl-image1.2-dev
install libsdl-sound1.2-dev libsdl-mixer1.2-dev
2 GLUT fue concebido para utilizarse en un entorno ms acadmico, simplificando el tratamiento de eventos
y la gestin de ventanas.
[197]
Para generar un fichero ejecutable, simplemente es necesario enlazar con las bibliotecas necesarias.
Listado 6.12: Ejemplo sencillo SDL + OpenGL.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <SDL/SDL.h>
#include <GL/gl.h>
#include <stdio.h>
int main (int argc, char *argv[]) {
SDL_Surface *screen;
// Inicializacin de SDL.
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "Unable to initialize SDL: %s\n",
SDL_GetError());
return -1;
}
// Cuando termine el programa, llamada a SQLQuit().
atexit(SDL_Quit);
// Activacin del double buffering.
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
// Establecimiento del modo de vdeo con soporte para OpenGL.
screen = SDL_SetVideoMode(640, 480, 16, SDL_OPENGL);
if (screen == NULL) {
fprintf(stderr, "Unable to set video mode: %s\n",
SDL_GetError());
return -1;
}
SDL_WM_SetCaption("OpenGL with SDL!", "OpenGL");
// Ya es posible utilizar comandos OpenGL!
glViewport(80, 0, 480, 480);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 100.0);
glClearColor(1, 1, 1, 0);
glMatrixMode(GL_MODELVIEW); glLoadIdentity();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Renderizado de un tringulo.
glBegin(GL_TRIANGLES);
glColor3f(1.0, 0.0, 0.0); glVertex3f(0.0, 1.0, -2.0);
glColor3f(0.0, 1.0, 0.0); glVertex3f(1.0, -1.0, -2.0);
glColor3f(0.0, 0.0, 1.0); glVertex3f(-1.0, -1.0, -2.0);
glEnd();
glFlush();
SDL_GL_SwapBuffers(); // Intercambio de buffers.
SDL_Delay(5000);
// Espera de 5 seg.
return 0;
}
[198]
CFLAGS := -c -Wall
LDFLAGS := sdl-config --cflags --libs -lSDL_image -lGL
LDLIBS := -lSDL_image -lGL
CC := gcc
all: basic_sdl_opengl
basic_sdl_opengl: basic_sdl_opengl.o
$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
basic_sdl_opengl.o: basic_sdl_opengl.c
$(CC) $(CFLAGS) $^ -o $@
clean:
@echo Cleaning up...
rm -f *~ *.o basic_sdl_opengl
@echo Done.
vclean: clean
Reproduccin de msica
En esta seccin se discute cmo implementar3 un nuevo recurso que permita la reproduccin de archivos de msica dentro de un juego. En este ejemplo se har uso de la
biblioteca SDL_mixer para llevar a cabo la reproduccin de archivos de sonido4 .
En primer lugar se define una clase Track que ser utilizada para gestionar el recurso
asociado a una cancin. Como ya se ha comentado anteriormente, esta clase hereda de
la clase Ogre::Resource, por lo que ser necesario implementar las funciones necesarias
para el nuevo tipo de recurso. Adems, se incluir la funcionalidad tpica asociada a una
cancin, como las clsicas operaciones play, pause o stop. El siguiente listado de cdigo
muestra la declaracin de la clase Track.
Listado 6.14: Clase Track.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <SDL/SDL_mixer.h>
#include <OGRE/Ogre.h>
class Track : public Ogre::Resource {
public:
// Constructor (ver Ogre::Resource).
Track (Ogre::ResourceManager* pManager,
const Ogre::String& resource_name,
Ogre::ResourceHandle handle,
const Ogre::String& resource_group,
bool manual_load = false,
Ogre::ManualResourceLoader* pLoader = 0);
~Track ();
// Manejo bsico del track.
void play (int loop = -1);
void pause ();
void stop ();
3 La implementacin de los recursos de sonido est basada en el artculo titulado Extender la gestin de
recursos, audio del portal IberOgre, el cual se encuentra disponible en http://osl2.uca.es/iberogre/index.
php/Extender_la_gestin_de_recursos, _audio.
4 SDL_mixer slo permite la reproduccin de un clip de sonido de manera simultnea, por lo que no ser
posible realizar mezclas de canciones.
[199]
void
Track::play
(int loop)
{
Ogre::LogManager* pLogManager =
Ogre::LogManager::getSingletonPtr();
if(Mix_PausedMusic()) // Estaba pausada?
Mix_ResumeMusic(); // Reanudacin.
// Si no, se reproduce desde el principio.
else {
if (Mix_PlayMusic(_pTrack, loop) == -1) {
pLogManager->logMessage("Track::play() Error al....");
throw (Ogre::Exception(Ogre::Exception::ERR_FILE_NOT_FOUND,
"Imposible reproducir...",
"Track::play()"));
}
}
}
[200]
void
Track::loadImpl () // Carga del recurso.
{
// Ruta al archivo.
Ogre::FileInfoListPtr info;
info = Ogre::ResourceGroupManager::getSingleton().
findResourceFileInfo(mGroup, mName);
for (Ogre::FileInfoList::const_iterator i = info->begin();
i != info->end(); ++i) {
_path = i->archive->getName() + "/" + i->filename;
}
if (_path == "") {
// Archivo no encontrado...
// Volcar en el log y lanzar excepcin.
}
// Cargar el recurso de sonido.
if ((_pTrack = Mix_LoadMUS(_path.c_str())) == NULL) {
// Si se produce un error al cargar el recurso,
// volcar en el log y lanzar excepcin.
}
// Clculo del tamao del recurso de sonido.
_size = ...
}
void
Track::unloadImpl()
{
if (_pTrack) {
// Liberar el recurso de sonido.
Mix_FreeMusic(_pTrack);
}
}
[201]
El listado de cdigo 6.17 muestra la implementacin de la clase TrackPtr, la cual incluye una serie de funciones (bsicamente constructores y asignador de copia) heredadas
de Ogre::SharedPtr. A modo de ejemplo, tambin se incluye el cdigo asociado al constructor de copia. Como se puede apreciar, en l se incrementa el contador de referencias
al recurso.
public:
// Es necesario implementar constructores y operador de asignacin.
4
5
TrackPtr(): Ogre::SharedPtr<Track>() {}
6
7
9
10
11
12
TrackPtr::TrackPtr
13
14
15
if (resource.isNull())
16
return;
17
// Para garantizar la exclusin mutua...
OGRE_LOCK_MUTEX(*resource.OGRE_AUTO_MUTEX_NAME)
OGRE_COPY_AUTO_SHARED_MUTEX(resource.OGRE_AUTO_MUTEX_NAME)
18
19
20
21
23
pRep = static_cast<Track*>(resource.getPointer());
pUseCount = resource.useCountPointer();
24
useFreeMethod = resource.freeMethod();
22
25
26
27
if (pUseCount)
++(*pUseCount);
28
29
Una vez implementada la lgica necesaria para instanciar y manejar recursos de sonido, el siguiente paso consiste en definir un gestor o manager especfico para centralizar
la administracin del nuevo tipo de recurso. Ogre3D facilita enormemente esta tarea gracias a la clase Ogre::ResourceManager. En el caso particular de los recursos de sonido se
define la clase TrackManager, cuyo esqueleto se muestra en el listado de cdigo 6.18.
Esta clase no slo hereda del gestor de recursos de Ogre, sino que tambin lo hace
de la clase Ogre::Singleton con el objetivo de manejar una nica instancia del gestor de
recursos de sonido. Las funciones ms relevantes son las siguientes:
load() (lneas 10-11 ), que permite la carga de canciones por parte del desarrollador. Si el recurso a cargar no existe, entonces lo crear internamente utilizando la
funcin que se comenta a continuacin.
[202]
// y Ogre::Singleton.
5
6
public:
TrackManager();
virtual ~TrackManager();
// Funcin de carga genrica.
10
11
12
13
14
15
protected:
16
17
18
19
20
bool isManual,
21
Ogre::ManualResourceLoader* loader,
const Ogre::NameValuePairList* createParams);
22
23
};
24
25
TrackPtr
26
TrackManager::load
27
28
29
30
31
32
33
if (trackPtr.isNull())
trackPtr = create(name, group);
34
35
36
37
trackPtr->load();
38
return trackPtr;
39
40
41
42
43
Ogre::Resource*
TrackManager::createImpl (const Ogre::String& resource_name,
44
Ogre::ResourceHandle handle,
const Ogre::String& resource_group,
45
46
47
bool isManual,
48
Ogre::ManualResourceLoader* loader,
const Ogre::NameValuePairList* createParams)
49
50
{
return new Track(this, resource_name, handle,
resource_group, isManual, loader);
51
52
53
[203]
Con las tres clases que se han discutido en esta seccin ya es posible realizar la carga
de recursos de sonido, delegando en la biblioteca SDL_mixer, junto con su gestin y administracin bsicas. Este esquema encapsula la complejidad del tratamiento del sonido,
por lo que en cualquier momento se podra sustituir dicha biblioteca por otra.
Ms adelante se muestra un ejemplo concreto en el que se hace uso de este tipo de
recursos sonoros. Sin embargo, antes de discutir este ejemplo de integracin se plantear
el soporte de efectos de sonido, los cuales se podrn mezclar con el tema o track principal
a la hora de desarrollar un juego. Como se plantear a continuacin, la filosofa de diseo
es exactamente igual que la planteada en esta seccin.
Soporte de efectos de sonido
Adems de llevar a cabo la reproduccin del algn tema musical durante la ejecucin
de un juego, la incorporacin de efectos de sonido es esencial para alcanzar un buen grado
de inmersin y que, de esta forma, el jugador se sienta como parte del propio juego. Desde
un punto de vista tcnico, este esquema implica que la biblioteca de desarrollo permita la
mezcla de sonidos. En el caso de SDL_mixer es posible llevar a cabo dicha integracin.
Como se ha comentado anteriormente, a efectos de implementacin, la integracin
de efectos de sonidos (FX effects) sigue el mismo planteamiento que el adoptado para la
gestin bsica de sonido. Para ello, en primer lugar se han creado las clases SoundFX
y SoundFXPtr, con el objetivo de gestionar y manipular las distintas instancias de los
efectos de sonido, respectivamente. En el siguiente listado de cdigo se muestra la clase SoundFX, que se puede entender como una simplificacin de la clase Track, ya que
los efectos de sonido se reproducirn puntualmente, mediante la funcin miembro play,
cuando as sea necesario.
La manipulacin de efectos de sonido se centraliza en la clase SoundFXManager,
la cual implementa el patrn Singleton y hereda de la clase Ogre::ResourceManager. La
diferencia ms sustancial con respecto al gestor de sonido reside en que el tipo de recurso
mantiene un identificador textual distinto y que, en el caso de los efectos de sonido, se
lleva a cabo una reserva explcita de 32 canales de audio. Para ello, se hace uso de una
funcin especfica de la biblioteca SDL_mixer.
Listado 6.19: Clase SoundFX.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[204]
TrackManager
1
1
SoundFXManager
MyApp
MyFrameListener
Figura 6.13: Diagrama simplificado de clases de las principales entidades utilizadas para llevar a cabo la integracin de msica y efectos de sonido.
bool
MyApp::initSDL () {
// Inicializando SDL...
if (SDL_Init(SDL_INIT_AUDIO) < 0)
return false;
// Llamar a SDL_Quit al terminar.
atexit(SDL_Quit);
// Inicializando SDL mixer...
if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,
MIX_DEFAULT_CHANNELS, 4096) < 0)
return false;
// Llamar a Mix_CloseAudio al terminar.
atexit(Mix_CloseAudio);
return true;
}
[205]
Es importante resaltar que a la hora de arrancar la instancia de la clase MyApp mediante la funcin start(), los gestores de sonido y de efectos se instancian. Adems, se
lleva a cabo la reproduccin del track principal.
Listado 6.21: Clase MyApp. Funcin start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Finalmente, slo hay que reproducir los eventos de sonido cuando as sea necesario.
En este ejemplo, dichos eventos se reproducirn cuando se indique, por parte del usuario,
el esquema de clculo de sombreado mediante las teclas 1 2. Dicha activacin se
realiza, por simplificacin, en la propia clase FrameListener; en concreto, en la funcin
miembro frameStarted a la hora de capturar los eventos de teclado.
Listado 6.22: Clase MyApp. Funcin frameStarted()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[206]
6.3.
El sistema de archivos
[207]
bin
boot
dev
etc
home
lib
david
luis
andrea
media
bin
usr
var
include
En el caso de las consolas, las rutas suelen seguir un convenio muy similar incluso para
referirse a distintos volmenes. Por ejemplo, PlayStation3TM utiliza el prefijo /dev_bdvd
para referirse al lector de blu-ray, mientras que el prefijo /dev_hddx permite el acceso a
un determinado disco duro.
Las rutas o paths pueden ser absolutas o relativas, en funcin de si estn definidas
teniendo como referencia el directorio raz del sistema de archivos u otro directorio, respectivamente. En sistemas UNIX, dos ejemplos tpicos seran los siguientes:
/usr/share/doc/ogre-doc/api/html/index.html
apps/firefox/firefox-bin
El primero de ellos sera una ruta absoluta, ya que se define en base al directorio raz.
Por otra parte, la segunda sera una ruta relativa, ya que se define en relacin al directorio
/home/david.
Antes de pasar a discutir aspectos de la gestin de E/S, resulta importante comentar
la existencia de APIs especficas para la gestin de rutas. Aunque la gestin bsica de
tratamiento de rutas se puede realizar mediante cadenas de texto, su complejidad hace
necesaria, normalmente, la utilizacin de APIs especficas para abstraer dicha complejidad. La funcionalidad relevante, como por ejemplo la obtencin del directorio, nombre
y extensin de un archivo, o la conversin entre paths absolutos y relativos, se puede
encapsular en una API que facilite la interaccin con este tipo de componentes.
[208]
API adicional para envolver a las APIs especficas para el tratamiento de archivos en
#include <iostream>
#include <boost/filesystem.hpp>
using namespace std;
using namespace boost::filesystem;
void list_directory (const path& dir, const int& tabs);
int main (int argc, char* argv[]) {
if (argc < 2) {
cout << "Uso: ./exec/Simple <path>" << endl;
return 1;
}
path p(argv[1]); // Instancia de clase boost::path.
if (is_regular_file(p))
cout << "
" << p << " " << file_size(p) << " B" << endl;
else if (is_directory(p)) // Listado recursivo.
list_directory(p, 0);
return 0;
}
void
print_tabs (const int& tabs) {
for (int i = 0; i < tabs; ++i) cout << "\t";
}
void list_directory
(const path& p, const int& tabs) {
vector<path> paths;
// directory iterator para iterar sobre los contenidos del dir.
copy(directory_iterator(p), directory_iterator(),
back_inserter(paths));
sort(paths.begin(), paths.end()); // Se fuerza el orden.
5 www.boost.org/libs/filesystem/
[209]
[210]
#include <stdio.h>
int lectura_sincrona (const char* archivo, char* buffer,
size_t tamanyo_buffer, size_t* p_bytes_leidos);
int main (int argc, const char* argv[]) {
char buffer[256];
size_t bytes_leidos = 0;
if (lectura_sincrona("test.txt", buffer, sizeof(buffer), &bytes_leidos))
printf(" %u bytes leidos!\n", bytes_leidos);
return 0;
}
int lectura_sincrona (const char* archivo, char* buffer,
size_t tamanyo_buffer, size_t* p_bytes_leidos) {
FILE* manejador = NULL;
if ((manejador = fopen(archivo, "rb"))) {
// Llamada bloqueante en fread,
// hasta que se lean todos los datos.
size_t bytes_leidos = fread(buffer, 1, tamanyo_buffer, manejador);
// Ignoramos errores...
fclose(manejador);
*p_bytes_leidos = bytes_leidos;
return 1;
}
return -1;
}
[211]
API E/S con buffers
Operacin
Abrir archivo
Cerrar archivo
Leer archivo
Desplazar
Obtener offset
Lectura lnea
Escritura lnea
Lectura cadena
Escritura cadena
Obtener estado
Signatura
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *fp);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE
*stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
int fscanf(FILE *stream, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int fstat(int fd, struct stat *buf);
Operacin
Abrir archivo
Cerrar archivo
Leer archivo
Escribir archivo
Desplazar
Obtener offset
Lectura lnea
Escritura lnea
Lectura cadena
Escritura cadena
Obtener estado
Escribir archivo
Tabla 6.1: Resumen de las principales operaciones de las APIs de C para la gestin de E/S (con y sin
buffers).
[212]
[213]
: Agente 1
Notificador
: Agente
Receptor
<<crear ()>>
: Objeto
callback
recibir_mensaje (cb, m)
Procesar
mensaje
responder ()
responder ()
Figura 6.18: Esquema grfico representativo de una comunicacin asncrona basada en objetos de retrollamada.
Desde un punto de vista general, la E/S asncrona se suele implementar mediante hilos
auxiliares encargados de atender las peticiones de E/S. De este modo, el hilo principal
ejecuta funciones que tendrn como consecuencia peticiones que sern encoladas para
atenderlas posteriormente. Este hilo principal retornar inmediatamente despus de llevar
a cabo la invocacin.
El hilo de E/S obtendr la siguiente peticin de la cola y la atender utilizando funciones de E/S bloqueantes, como la funcin fread discutida en el anterior listado de cdigo. Cuando se completa una peticin, entonces se invoca al objeto o a la funcin de
retrollamada proporcionada anteriormente por el hilo principal (justo cuando se realiz la
peticin inicial). Este objeto o funcin notificar la finalizacin de la peticin.
En caso de que el hilo principal tenga que esperar a que la peticin de E/S se complete
antes de continuar su ejecucin ser necesario proporcionar algn tipo de mecanismo
para garantizar dicho bloqueo. Normalmente, se suele hacer uso de semforos [14] (ver
figura 6.20), vinculando un semforo a cada una de las peticiones. De este modo, el hilo
principal puede hacer uso de wait hasta que el hilo de E/S haga uso de signal, posibilitando
as que se reanude el flujo de ejecucin.
[214]
tiempo de ejecucin sea elevado. En el caso del desarrollo de videojuegos, una posible aplicacin de este enfoque, tal y como se ha introducido
anteriormente, sera la carga en segundo plano de recursos que sern utilizados en el futuro inmediato. La situacin tpica en este contexto sera la carga de los datos del siguiente
nivel de un determinado juego.
wait
signal
Facilidad de uso, ya que la biblioteca est orientada a proporcionar un kit de herramientas, en lugar
de basarse en el modelo framework.
La instalacin de la biblioteca Boost.Asio en sistemas
Debian y derivados es trivial mediante los siguientes comandos:
$ sudo apt-get update
$ sudo apt-get install libasio-dev
7 http://www.boost.org/doc/libs/1_48_0/doc/html/boost_asio.html
[215]
#include <iostream>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
int main () {
// Todo programa que haga uso de asio ha de instanciar
// un objeto del tipo io service para manejar la E/S.
boost::asio::io_service io;
// Instancia de un timer (3 seg.)
// El primer argumento siempre es un io service.
boost::asio::deadline_timer t(io, boost::posix_time::seconds(3));
// Espera explcita.
t.wait();
std::cout << "Hola Mundo!" << std::endl;
return 0;
}
Para compilar, enlazar y generar el archivo ejecutable, simplemente es necesario ejecutar las siguientes instrucciones:
$ g++ Simple.cpp -o Simple -lboost_system -lboost_date_time
$ ./Simple
En determinados contextos resulta muy deseable llevar a cabo una espera asncrona,
es decir, continuar ejecutando instrucciones mientras en otro nivel de ejecucin se realizan otras tareas. Si se utiliza la biblioteca Boost.Asio, es posible definir manejadores de
cdigo asociados a funciones de retrollamada que se ejecuten mientras el programa contina la ejecucin de su flujo principal. Asio tambin proporciona mecanismos para que
el programa no termine mientras haya tareas por finalizar.
El listado de cdigo 6.26 muestra cmo el ejemplo anterior se puede modificar para
llevar a cabo una espera asncrona, asociando en este caso un temporizador que controla
la ejecucin de una funcin de retrollamada.
La ejecucin de este programa generar la siguiente salida:
$ Esperando a print()...
$ Hola Mundo!
15 ) se ejecutar
que inmediatamente despus de la llamada
a
la
espera
asncrona
(lnea
8 La nocin de segundo plano en este contexto est vinculada a independizar ciertas tareas de la ejecucin
del flujo principal de un programa.
[216]
#include <iostream>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
// Funcin de retrollamada.
void print (const boost::system::error_code& e) {
std::cout << "Hola Mundo!" << std::endl;
}
int main () {
boost::asio::io_service io;
boost::asio::deadline_timer t(io, boost::posix_time::seconds(3));
// Espera asncrona.
t.async_wait(&print);
std::cout << "Esperando a print()..." << std::endl;
// Pasarn casi 3 seg. hasta que print se ejecute...
// io.run() para garantizar que los manejadores se llamen desde
// hilos que llamen a run().
// run() se ejecutar mientras haya cosas por hacer,
// en este caso la espera asncrona a print.
io.run();
return 0;
}
Otra opcin imprescindible a la hora de gestionar estos manejadores reside en la posibilidad de realizar un paso de parmetros, en funcin del dominio de la aplicacin que
se est desarrollando. El siguiente listado de cdigo muestra cmo llevar a cabo dicha
tarea.
Listado 6.27: Biblioteca Asio. Espera asncrona y paso de parmetros.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define THRESHOLD 5
// Funcin de retrollamada con paso de parmetros.
void count (const boost::system::error_code& e,
boost::asio::deadline_timer* t, int* counter) {
if (*counter < THRESHOLD) {
std::cout << "Contador... " << *counter << std::endl;
++(*counter);
// El timer se prolonga un segundo...
t->expires_at(t->expires_at() + boost::posix_time::seconds(1));
// Llamada asncrona con paso de argumentos.
t->async_wait(boost::bind
(count, boost::asio::placeholders::error, t, counter));
}
}
int main () {
int counter = 0;
boost::asio::deadline_timer t(io, boost::posix_time::seconds(1));
// Llamada inicial.
t.async_wait(boost::bind
(count, boost::asio::placeholders::error,
&t, &counter));
// ...
}
[217]
Como se puede apreciar, las llamadas a la funcin async_wait() varan con respecto
a otros ejemplos, ya que se indica de manera explcita los argumentos relevantes para la
funcin de retrollamada. En este caso, dichos argumentos son la propia funcin de retrollamada, para realizar llamadas recursivas, el timer para poder prolongar en un segundo su
duracin en cada llamada, y una variable entera que se ir incrementando en cada llamada.
La biblioteca Boost.Asio tambin permite encapsular las funciones de retrollamada que se ejecutan de manera asncrona como funciones miembro de una clase. Este esquema mejora el diseo
de la aplicacin y permite que el desarrollador se
abstraiga de la implementacin interna de la clase.
El siguiente listado de cdigo muestra una posible
modificacin del ejemplo anterior mediante la definicin de una clase Counter, de manera que la funcin count pasa a ser una funcin
miembro de dicha clase.
Flexibilidad en Asio
La biblioteca Boost.Asio es muy flexible y posibilita la llamada asncrona a una funcin que acepta un nmero arbitrario de parmetros. stos pueden ser variables, punteros, o funciones, entre otros.
class Counter {
public:
Counter (boost::asio::io_service& io)
: _timer(io, boost::posix_time::seconds(1)), _count(0) {
_timer.async_wait(boost::bind(&Counter::count, this));
}
~Counter () { cout << "Valor final: " << _count << endl; }
void count () {
if (_count < 5) {
std::cout << _count++ << std::endl;
_timer.expires_at(_timer.expires_at() +
boost::posix_time::seconds(1));
// Manejo de funciones miembro.
_timer.async_wait(boost::bind(&Counter::count, this));
}
}
private:
boost::asio::deadline_timer _timer;
int _count;
};
int main () {
boost::asio::io_service io;
Counter c(io); // Instancia de la clase Counter.
io.run();
return 0;
}
[218]
En este caso, la biblioteca proporciona wrappers para envolver las funciones de retrollamada count1 y count2, con el objetivo de que, internamente, Asio controle que dichos
manejadores no se ejecutan de manera concurrente. En el ejemplo propuesto, la variable
miembro _count no se actualizar, simultneamente, por dichas funciones miembro.
Listado 6.29: Biblioteca Asio. Espera asncrona y clases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Counter {
public:
Counter (boost::asio::io_service& io)
: _strand(io), // Para garantizar la exclusin mutua.
_timer1(io, boost::posix_time::seconds(1)),
_timer2(io, boost::posix_time::seconds(1)),
_count(0) {
// Los manejadores se envuelven por strand para
// que no se ejecuten de manera concurrente.
_timer1.async_wait(_strand.wrap
(boost::bind(&Counter::count1, this)));
_timer2.async_wait(_strand.wrap
(boost::bind(&Counter::count2, this)));
}
// count1 y count2 nunca se ejecutarn en paralelo.
void count1() {
if (_count < 10) {
// IDEM que en el ejemplo anterior.
_timer1.async_wait(_strand.wrap
(boost::bind(&Counter::count1, this)));
}
}
// IDEM que count1 pero sobre timer2 y count2
void count2() { /* src */ }
private:
boost::asio::strand _strand;
boost::asio::deadline_timer _timer1, _timer2;
int _count;
};
int main() {
boost::asio::io_service io;
// run() se llamar desde dos threads (principal y boost)
Counter c(io);
boost::thread t(boost::bind(&boost::asio::io_service::run, &io));
io.run();
t.join();
return 0;
}
[219]
6.4.
En esta seccin se justifica la necesidad de plantear esquemas de datos para importar contenido desde entornos de creacin de contenido 3D, como por ejemplo Blender.
Aunque existen diversos estndares para la definicin de datos multimedia, en la prctica
cada aplicacin grfica interactiva tiene necesidades especficas que han de cubrirse con
un software particular para importar dichos datos.
Por otra parte, el formato de los datos es otro aspecto relevante, existiendo distintas
aproximaciones para codificar la informacin multimedia a importar. Uno de los esquemas ms utilizados consiste en hacer uso del metalenguaje XML (eXtensible Markup
Language), por lo que en este captulo se discutir esta opcin en detalle. XML mantiene
un alto nivel semntico, est bien soportado y estandarizado y permite asociar estructuras
de rbol bien definidas para encapsular la informacin relevante. Adems, posibilita que
el contenido a importar sea legible por los propios programadores.
Es importante resaltar que este captulo est centrado en la parte de importacin de
datos hacia el motor de juegos, por lo que el proceso de exportacin no se discutir a
continuacin (s se har, no obstante, en el mdulo 2 de Programacin Grfica).
[220]
Ogre Exporter
Importer
Figura 6.21: Procesos de importancin y exportacin de datos 3D haciendo uso de documentos XML.
Aunque el uso del metalenguaje XML es una de las opciones ms extendidas, a continuacin se discute brevemente el uso de otras alternativas.
Archivos binarios
El mismo planteamiento discutido que se usa para llevar a cabo el almacenamiento
de las variables de configuracin de un motor de juegos, utilizando un formato binario,
se puede aplicar para almacenar informacin multimedia con el objetivo de importarla
posteriormente.
Este esquema basado en un formato binario es muy eficiente y permite hacer uso
de tcnicas de serializacin de objetos, que a su vez incrementan la portabilidad y la
sencillez de los formatos binarios. En el caso de hacer uso de orientacin a objetos, la
funcionalidad necesaria para serializar y de-serializar objetos se puede encapsular en la
propia definicin de clase.
Es importante destacar que la serializacin de objetos puede generar informacin en
formato XML, es decir, no tiene por qu estar ligada a un formato binario.
Archivos en texto plano
El uso de archivos en texto plano es otra posible alternativa a la hora de importar
datos. Sin embargo, esta aproximacin tiene dos importantes desventajas: i) resulta menos
eficiente que el uso de un formato binario e ii) implica la utilizacin de un procesador de
texto especfico.
En el caso de utilizar un formato no estandarizado, es necesario explicitar los separadores o tags existentes entre los distintos campos del archivo en texto plano. Por ejemplo,
se podra pensar en separadores como los dos puntos, el punto y coma y el retorno de
carro para delimitar los campos de una determinada entidad y una entidad de la siguiente,
respectivamente.
XML
EXtensible Markup Language (XML) se suele definir como un metalenguaje, es decir, como un lenguaje que permite la definicin de otros lenguajes. En el caso particular
de este captulo sobre datos de intercambio, XML se podra utilizar para definir un lenguaje propio que facilite la importacin de datos 3D al motor de juegos o a un juego en
particular.
Actualmente, XML es un formato muy popular debido a los siguientes motivos:
Es un estndar.
Est muy bien soportado, tanto a nivel de programacin como a nivel de usuario.
[221]
Es legible.
Tiene un soporte excelente para estructuras de datos jerrquicas; en concreto, de
tipo arbreas. Esta propiedad es especialmente relevante en el dominio de los videojuegos.
Sin embargo, no todo son ventajas. El proceso de parseado o parsing es relativamente lento. Esto implica que
algunos motores hagan uso de formatos binarios propieSGML
tarios, los cuales son ms rpidos de parsear y mucho
ms compactos que los archivos XML, reduciendo as los
tiempos de importacin y de carga.
HTML
XML
El parser Xerces-C++
Como se ha comentado anteriormente, uno de los motivos por los que XML est tan extendido es su amplio
XHTML
soporte a nivel de programacin. En otras palabras, prcticamente la mayora de lenguajes de programacin pro- Figura 6.22: Visin conceptual de la
porcionan bibliotecas, APIs y herramientas para procesar relacin de XML con otros lenguajes.
y generar contenidos en formato XML.
En el caso del lenguaje C++, estndar de facto en la
industria del videojuego, el parser Xerces-C++11 es una
de las alternativas ms completas para llevar a cabo el
procesamiento de ficheros XML. En concreto, esta herramienta forma parte del proyecto Apache XML, el cual
gestiona un nmero relevante de subproyectos vinculados
al estndar XML.
Xerces-C++ est escrito en un subconjunto portable
de C++ y tiene como objetivo facilitar la lectura y escritura de archivos XML. Desde un punto de vista tcnico,
Xerces-C++ proporciona una biblioteca con funcionalidad para parsear, generar, manipular y validar documen6.23: La instalacin de patos XML utilizando las APIs DOM, SAX (Simple API for Figura
quetes en sistemas Debian tambin
XML) y SAX2. Estas APIs son las ms populares para se puede realizar a travs de gestores
manipular documentos XML y difieren, sin competir en- de ms alto nivel, como por ejemplo
tre s, en aspectos como el origen, alcance o estilo de pro- Synaptic.
gramacin.
Por ejemplo, mientras DOM (Document Object Model) est siendo desarrollada por el
consorcio W3, SAX (Simple API for XML) no lo est. Sin embargo, la mayor diferencia
reside en el modelo de programacin, ya que SAX presenta el documento XML como
una cadena de eventos serializada, mientras que DOM lo trata a travs de una estructura
de rbol. La principal desventaja del enfoque planteado en SAX es que no permite un
acceso aleatorio a los elementos del documento. No obstante, este enfoque posibilita que
el desarrollador no se preocupe de informacin irrelevante, reduciendo as el tamao en
memoria necesario por el programa.
La instalacin de Xerces-C++, incluyendo documentacin y ejemplos de referencia,
en sistemas Debian y derivados es trivial mediante los siguientes comandos:
$
$
$
$
sudo
sudo
sudo
sudo
apt-get
apt-get
apt-get
apt-get
update
install libxerces-c-dev
install libxerces-c-doc
install libxerces-samples
11 http://xerces.apache.org/xerces-c/
[222]
Por una parte, en los vrtices del grafo se incluye su posicin en el espacio 3D mediante las
etiquetas <x>, <y> y <z>. Adems, cada vrtice
tiene como atributo un ndice, que lo identifica unvocamente, y un tipo, el cual puede ser spawn (punto de generacin) o drain (punto de desaparicin),
respectivamente. Por otra parte, los arcos permiten
definir la estructura concreta del grafo a importar mediante la etiqueta <vertex>.
Vrtices y arcos
En el formato definido, los vrtices se
identifican a travs del atributo index.
Estos IDs se utilizan en la etiqueta
<edge> para especificar los dos vrtices que conforman un arco.
[223]
Figura 6.24: Representacin interna bidimensional en forma de grafo del escenario de NoEscapeDemo. Los
nodos de tipo S (spawn) representan puntos de nacimiento, mientras que los nodos de tipo D (drain) representan
sumideros, es decir, puntos en los que los fantasmas desaparecen.
Listado 6.30: Ejemplo de fichero XML usado para exportar e importar contenido asociado al grafo
del escenario.
1
<data>
3
4
<graph>
5
6
7
</vertex>
10
11
12
13
14
15
<edge>
<vertex>1</vertex> <vertex>2</vertex>
16
</edge>
17
<edge>
18
<vertex>2</vertex> <vertex>4</vertex>
19
</edge>
20
21
22
</graph>
23
24
25
En caso de que sea necesario incluir ms contenido, simplemente habr que extender
el formato definido. XML facilita enormemente la escalabilidad gracias a su esquema
basado en el uso de etiquetas y a su estructura jerrquica.
[224]
El siguiente listado muestra la otra parte de la estructura del documento XML utilizado
para importar contenido. ste contiene la informacin relativa a las cmaras virtuales.
Como se puede apreciar, cada cmara tiene asociado un camino, definido con la etiqueta
<path>, el cual consiste en una serie de puntos o frames clave que determinan la animacin de la cmara.
Cada punto clave del camino de una cmara contiene la posicin de la misma en el
espacio 3D (etiqueta <frame>) y la rotacin asociada, expresada mediante un cuaternin
(etiqueta <rotation>).
Listado 6.31: Ejemplo de fichero XML usado para exportar e importar contenido asociado a las
cmaras virtuales.
1
<data>
4
5
6
7
<path>
8
9
10
11
<frame index="1">
<position>
<x>1.5</x> <y>2.5</y> <z>-3</z>
12
</position>
13
<rotation>
14
15
</rotation>
16
</frame>
17
18
19
20
<frame index="2">
<position>
<x>2.5</x> <y>2.5</y> <z>-3</z>
21
</position>
22
<rotation>
23
24
</rotation>
25
</frame>
26
27
28
29
</path>
30
31
</camera>
32
33
34
Lgica de dominio
La figura 6.25 muestra el diagrama de las principales clases vinculadas al importador
de datos. Como se puede apreciar, las entidades ms relevantes que forman parte del
documento XML se han modelado como clases con el objetivo de facilitar no slo la
obtencin de los datos sino tambin su integracin en el despliegue final con Ogre3D.
[225]
La clase que centraliza la lgica de dominio es la clase Scene, la cual mantiene una relacin de asociacin con las clases Graph y Camera. Recuerde que el contenido a importar
ms relevante sobre el escenario era la estructura de grafo del mismo y la informacin de
las cmaras virtuales. La encapsulacin de los datos importados de un documento XML
se centraliza en la clase Scene, la cual mantiene como estado un puntero a un objeto de
tipo Graph y una estructura con punteros a objetos de tipo Camera. El listado de cdigo
6.32 muestra la declaracin de la clase Scene.
Listado 6.32: Clase Scene.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include
#include
#include
#include
<vector>
<Camera.h>
<Node.h>
<Graph.h>
class Scene
{
public:
Scene ();
~Scene ();
void addCamera (Camera* camera);
Graph* getGraph () { return _graph;}
std::vector<Camera*> getCameras () { return _cameras; }
private:
Graph *_graph;
std::vector<Camera*> _cameras;
};
Por otra parte, la clase Graph mantiene la lgica de gestin bsica para implementar
una estructura de tipo grafo mediante listas de adyacencia. El siguiente listado de cdigo
muestra dicha clase e integra una funcin miembro para obtener
la lista de vrtices o
nodos adyacentes a partir del identificador de uno de ellos (lnea 17 ).
Listado 6.33: Clase Graph.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include
#include
#include
#include
<iostream>
<vector>
<GraphVertex.h>
<GraphEdge.h>
class Graph {
public:
Graph ();
~Graph ();
void addVertex (GraphVertex* pVertex);
void addEdge (GraphVertex* pOrigin, GraphVertex* pDestination,
bool undirected = true);
// Lista de vrtices adyacentes a uno dado.
std::vector<GraphVertex*> adjacents (int index);
GraphVertex* getVertex (int index);
std::vector<GraphVertex*> getVertexes () const
{return _vertexes;}
std::vector<GraphEdge*> getEdges () const { return _edges; }
private:
std::vector<GraphVertex*> _vertexes;
std::vector<GraphEdge*> _edges;
};
[226]
Scene
1
Importer
Ogre::Singleton
1..*
Camera
1
1
Graph
1..*
GraphVertex
2
1
0..*
1..*
Frame
0..*
GraphEdge
1
Node
Figura 6.25: Diagrama de clases de las entidades ms relevantes del importador de datos.
Respecto al diseo de las cmaras virtuales, las clases Camera y Frame son las utilizadas para encapsular la informacin y funcionalidad de las mismas. En esencia, una cmara consiste en un identificador, un atributo que determina la tasa de frames por segundo
a la que se mueve y una secuencia de puntos clave que conforman el camino asociado a
la cmara.
[227]
La clase Importer
El punto de interaccin entre los datos contenidos en el documento XML y la lgica de dominio previamente discutida est representado por la clase Importer. Esta clase
proporciona la funcionalidad necesaria para parsear documentos XML con la estructura planteada anteriormente y rellenar las estructuras de datos diseadas en la anterior
seccin.
El siguiente listado de cdigo muestra la declaracin de la clase Importer. Como se
puede apreciar, dicha clase hereda de Ogre::Singleton para garantizar que solamente existe una instancia de dicha clase, accesible con las funciones miembro getSingleton() y
getSingletonPtr()12 .
Note cmo, adems deestas dos funciones miembro, la nica funcin miembro pblica es parseScene() (lnea 9 ), la cual se puede utilizar para parsear un documento XML
cuya ruta se especifica en el primer parmetro. El efecto de realizar una llamada a esta
funcin tendr como resultado el segundo parmetro de la misma, de tipo puntero a objeto de clase Scene, con la informacin obtenida a partir del documento XML (siempre y
cuando no se produzca ningn error).
Listado 6.35: Clase Importer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <OGRE/Ogre.h>
#include <xercesc/dom/DOM.hpp>
#include <Scene.h>
class Importer: public Ogre::Singleton<Importer> {
public:
// nica funcin miembro pblica para parsear.
void parseScene (const char* path, Scene *scn);
static Importer& getSingleton ();
static Importer* getSingletonPtr ();
// Ogre::Singleton.
// Ogre::Singleton.
private:
// Funcionalidad oculta al exterior.
// Facilita el parseo de las diversas estructuras
// del documento XML.
void parseCamera (xercesc::DOMNode* cameraNode, Scene* scn);
void addPathToCamera (xercesc::DOMNode* pathNode, Camera *cam);
void getFramePosition (xercesc::DOMNode* node,
Ogre::Vector3* position);
void getFrameRotation (xercesc::DOMNode* node,
Ogre::Vector4* rotation);
void parseGraph (xercesc::DOMNode* graphNode, Scene* scn);
void addVertexToScene (xercesc::DOMNode* vertexNode, Scene* scn);
void addEdgeToScene (xercesc::DOMNode* edgeNode, Scene* scn);
// Funcin auxiliar para recuperar valores en punto flotante
// asociados a una determinada etiqueta (tag).
float getValueFromTag (xercesc::DOMNode* node, const XMLCh *tag);
};
El cdigo del importador se ha estructurado de acuerdo a la definicin del propio documento XML, es decir, teniendo en cuenta las etiquetas ms relevantes dentro del mismo.
As, dos de las funciones miembro privadas relevantes son, por ejemplo, las funciones parseCamera() y parseGraph(), usadas para procesar la informacin de una cmara virtual y
del grafo que determina el escenario de la demo.
12 Se
[228]
La biblioteca Xerces-C++ hace uso de tipos de datos especficos para, por ejemplo,
manejar cadenas de texto o los distintos nodos del rbol cuando se hace uso del API
DOM. De hecho, el API utilizada en este importador es DOM. El siguiente listado de
cdigo muestra cmo se ha realizado el procesamiento inicial del rbol asociado al documento XML. El rbol generado por el API DOM a la hora de parsear un documento XML
puede ser costoso en memoria. En ese caso, se podra plantear otras opciones, como por
ejemplo el uso del API SAX.
Aunque se ha omitido gran parte del cdigo, incluido el tratamiento de errores, el
esquema de procesamiento representa muy bien la forma de manejar este tipo de documentos haciendo uso del API DOM. Bsicamente,
la idea general consiste en obtener
una referencia a la raz del documento (lneas 13-14 ) y, a partir de ah, ir recuperando la
informacin contenida en cada uno de los nodos del mismo.
El bucle for (lneas 20-21 ) permite recorrer los nodos hijo del nodo raz. Si se detecta
un nodo con la etiqueta <camera>, entonces se llama a la funcin miembro parseCamera(). Si por el contrario se encuentra la etiqueta <graph>, entonces se llama a la funcin
parseGraph(). En ambos casos, estas funciones irn poblando de contenido el puntero a
objeto de tipo Scene que se pasa como segundo argumento.
Listado 6.36: Clase Importer. Funcin parseScene
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Estructurando cdigo... Una buena programacin estructurada es esencial para facilitar el mantenimiento del cdigo y balancear de manera adecuada la complejidad
de las distintas funciones que forman parte del mismo.
[230]
En los ltimos aos, el desarrollo de los procesadores, tanto de ordenadores personales, consolas de sobremesa e incluso telfonos mviles, ha estado marcado por un modelo
basado en la integracin de varios ncleos fsicos de ejecucin. El objetivo principal de
este diseo es la paralelizacin de las tareas a ejecutar por parte del procesador, permitiendo as incrementar el rendimiento global del sistema a travs del paralelismo a nivel
de ejecucin.
Este esquema, unido al concepto de hilo como unidad bsica de ejecucin de la CPU,
ha permitido que aplicaciones tan exigentes como los videojuegos se pueden aprovechar
de esta sustancial mejora de rendimiento. Desafortunadamente, no todo son buenas noticias. Este modelo de desarrollo basado en la ejecucin concurrente de mltiples hilos
de control tiene como consecuencia directa el incremento de la complejidad a la hora de
desarrollar dichas aplicaciones.
Adems de plantear soluciones que sean paralelizables y escalables respecto al nmero de unidades de procesamiento, un aspecto crtico a considerar es el acceso
CPU
CPU
concurrente a los datos. Por ejemplo, si dos hilos de ejecucin distintos comparten un fragmento de datos sobre
L1 Cach
L1 Cach
el que leen y escriben indistintamente, entonces el programador ha de integrar soluciones que garanticen la consistencia de los datos, es decir, soluciones que eviten situaciones en las que un hilo est escribiendo sobre dichos
L2 Cach
datos y otro est accediendo a los mismos.
Con el objetivo de abordar esta problemtica desde un punto de vista prctico en el mbito del desarrollo de videojuegos, en este captulo se plantean distintos mecanismos de sincronizacin de hilos haciendo
L1 Cach
L1 Cach
uso de los mecanismos nativos ofrecidos en C++11
y, por otra parte, de la biblioteca de hilos de ZeroC
CPU
CPU
I CE, un middleware de comunicaciones que proporcioFigura 7.1: Esquema general de una na una biblioteca de hilos que abstrae al desarrollaCPU con varios procesadores.
dor de la plataforma y el sistema operativo subyacentes.
7.1.
[231]
Manejo de
cadenas
Gestin de
memoria
Gestin de
archivos
Biblioteca
matemtica
...
Subsistemas principales
[232]
class MemoryManager {
public:
MemoryManager () {
// Inicializacin gestor de memoria...
4
5
~MemoryManager () {
// Parada gestor de memoria...
7
8
9
10
11
// ...
};
12
13
// Instancia nica.
14
Variables globales. Recuerde que idealmente las variables globales se deberan limitar en la medida de lo posible con el objetivo de evitar efectos colaterales.
class MemoryManager {
public:
static MemoryManager& get () {
static MemoryManager gMemoryManager;
return gMemoryManager;
}
MemoryManager () {
// Arranque de otros subsistemas dependientes...
SubsistemaX::get();
SubsistemaY::get();
// Inicializacin gestor de memoria...
}
~MemoryManager () {
// Parada gestor de memoria...
}
// ...
};
[233]
Una solucin directa a esta problemtica consiste en declarar la variable esttica dentro de una funcin, con el objetivo de obtenerla cuando as sea necesario. De este modo,
dicha instancia no se instanciar antes de la funcin main, sino que lo har cuando se efecte la primera llamada a la funcin implementada. El anterior listado de cdigo muestra
una posible implementacin de esta solucin.
Variables no locales. La inicializacin de variables estticas no locales se controla
mediante el mecanismo que utilice la implementacin para arrancar un programa en
C++.
Una posible variante de este diseo consiste en reservar memoria de manera dinmica
para la instancia nica del gestor en cuestin, tal y como se muestra en el siguiente listado
de cdigo.
Listado 7.3: Subsistema de gestin de memoria. Reserva dinmica.
1
2
3
4
5
6
7
8
9
[234]
class MemoryManager {
public:
MemoryManager () {}
~MemoryManager () {}
void
//
}
void
//
}
};
startUp () {
Inicializacin gestor de memoria...
shutDown () {
Parada gestor de memoria...
Recuerde que la simplicidad suele ser deseable en la mayora de los casos, aunque
el rendimiento o el propio diseo de un programa se vean degradados. Este enfoque
facilita el mantenimiento del cdigo. El subsistema de arranque y parada es un caso
tpico en el mbito de desarrollo de videojuegos.
[235]
int main () {
gMemoryManager.startUp();
gTextureManager.startUp();
gRenderManager.startUp();
gAnimationManager.startUp();
// ...
8
// Bucle principal.
gSimulationManager.run();
10
11
12
13
// ...
14
gAnimationManager.startUp();
15
gRenderManager.startUp();
16
gTextureManager.startUp();
17
gMemoryManager.startUp();
18
return 0;
19
20
[236]
Ogre::Singleton<Root>
RootAlloc
Ogre::Root
Figura 7.5: Clase Ogre::Root y su relacin con las clases Ogre::Singleton y RootAlloc.
protected:
// Ms declaraciones...
4
5
LogManager* mLogManager;
ControllerManager* mControllerManager;
SceneManagerEnumerator* mSceneManagerEnum;
DynLibManager* mDynLibManager;
8
9
ArchiveManager* mArchiveManager;
MaterialManager* mMaterialManager;
10
11
MeshManager* mMeshManager;
ParticleSystemManager* mParticleManager;
12
13
SkeletonManager* mSkeletonManager;
OverlayElementFactory* mPanelFactory;
14
15
OverlayElementFactory* mBorderPanelFactory;
OverlayElementFactory* mTextAreaFactory;
16
17
OverlayManager* mOverlayManager;
FontManager* mFontManager;
18
19
ArchiveFactory *mZipArchiveFactory;
ArchiveFactory *mFileSystemArchiveFactory;
20
21
ResourceGroupManager* mResourceGroupManager;
ResourceBackgroundQueue* mResourceBackgroundQueue;
22
23
ShadowTextureManager* mShadowTextureManager;
RenderSystemCapabilitiesManager* mRenderSystemCapabilitiesManager;
24
25
ScriptCompilerManager *mCompilerManager;
26
// Ms declaraciones...
27
28
};
[237]
Desde el punto de vista del renderizado, el objeto de tipo Root proporciona la funcin
startRendering. Cuando se realiza una llamada a dicha funcin, la aplicacin entrar en
un bucle de renderizado continuo que finalizar cuando todas las ventanas grficas se
hayan cerrado o cuando todos los objetos del tipo FrameListener finalicen su ejecucin
(ver mdulo 2, Programacin Grfica).
La implementacin del constructor de la clase Ogre::Root tiene como objetivo principal instanciar y arrancar los distintos subsistemas previamente declarados en el anterior
listado de cdigo. A continuacin se muestran algunos aspectos relevantes de la implementacin de dicho constructor.
2
3
// Inicializaciones...
// Inicializacin.
// src ...
8
// Creacin del log manager y archivo de log por defecto.
if(LogManager::getSingletonPtr() == 0) {
mLogManager = OGRE_NEW LogManager();
10
11
12
}
13
14
15
16
17
18
// ResourceGroupManager.
mResourceGroupManager = OGRE_NEW ResourceGroupManager();
19
20
21
// src...
22
23
// Material manager.
mMaterialManager = OGRE_NEW MaterialManager();
24
25
// Mesh manager.
mMeshManager = OGRE_NEW MeshManager();
26
27
// Skeleton manager.
mSkeletonManager = OGRE_NEW SkeletonManager();
28
29
30
31
// src...
32
33
Start your engine! Aunque el arranque y la parada de los motores de juegos actuales suelen estar centralizados mediante la gestin de algn elemento central, como
por ejemplo la clase Ogre::Root, es responsabilidad del desarrollador conocer los
elementos accesibles desde dicha entidad de control central.
[239]
Recuerde que Quake III est desarrollado utilizando el lenguaje C. El cdigo fuente
est bien diseado y estructurado. Para tener una visin global de su estructura se
recomienda visualizar el archivo g_local.h, en el que se explicita el fichero de las
funciones ms relevantes del cdigo.
A continuacin se muestran los aspectos ms destacados de la funcin de inicializacin de Quake III. Como se puede apreciar, la funcin G_InitGame() se encarga de
inicializar los aspectos ms relevantes a la hora de arrancar el juego. Por ejemplo, se
inicializan punteros relevantes para manejar elementos del juego en G_InitMemory(), se
resetean valores si la sesin de juego ha cambiado en G_InitWorldSession(), se inicializan
las entidades del juego, etctera.
2
3
G_InitMemory();
4
5
// ...
6
7
G_InitWorldSession();
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
InitBodyQue();
26
// Inicializacin general.
G_FindTeams();
27
28
// ...
29
30
[240]
vwMain ()
G_InitGame ()
G_RunFrame ()
G_ShutdownGame ()
Figura 7.7: Visin abstracta del flujo general de ejecucin de Quake III Arena (no se considera la conexin y
desconexin de clientes).
Listado 7.10: Quake 3. Funcin G_ShutdownGame.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
7.2. Contenedores
[241]
Programacin estructurada. El cdigo de Quake es un buen ejemplo de programacin estructurada que gira en torno a una adecuada definicin de funciones y a un
equilibrio respecto a la complejidad de las mismas.
7.2.
Contenedores
7.2.1. Iteradores
Desde un punto de vista abstracto, un iterador se puede definir como una clase que permite acceder de manera eficiente a los elementos de un contenedor especfico. Segn [17],
un iterador es una abstraccin pura, es decir, cualquier elemento que se comporte como
un iterador se define como un iterador. En otras palabras, un iterador es una abstraccin
del concepto de puntero a un elemento de una secuencia. Normalmente, los iteradores se
implementan haciendo uso del patrn que lleva su mismo nombre, tal y como se discuti
en la seccin 4.5.3. Los principales elementos clave de un iterador son los siguientes:
El elemento al que apunta (desreferenciado mediante los operadores * y ->).
La posibilidad de apuntar al siguiente elemento (incrementndolo mediante el operador ++).
La igualdad (representada por el operador ==).
De este modo, un elemento primitivo int* se puede definir como un iterador sobre un
array de enteros, es decir, sobre int[]. As mismo, std::list<std::string>::iterator es un
iterador sobre la clase list.
Un iterador representa la abstraccin de un puntero dentro de un array, de manera que
no existe el concepto de iterador nulo. Como ya se introdujo anteriormente, la condicin
para determinar si un iterador apunta o no a un determinado elemento se evala mediante
una comparacin al elemento final de una secuencia (end).
Rasgos de iterador. En STL, los tipos relacionados con un iterador se describen
a partir de una serie de declaraciones en la plantilla de clase iterator_traits. Por
ejemplo, es posible acceder al tipo de elemento manejado o el tipo de las operaciones
soportadas.
[242]
Acceso
directo
Bidireccional
Unidireccional
Entrada
Salida
Figura 7.8: Esquema grfica de las relaciones entre las distintas categoras de iteradores en STL. Cada categora
implementa la funcionalidad de todas las categoras que se encuentran a su derecha.
Categora
Lectura
Acceso
Escritura
Iteracin
Comparacin
Acceso directo
=*p
->[]
*p=
++ + - += -=
== != <><= >=
Tabla 7.1: Resumen de las principales categoras de iteradores en STL junto con sus operaciones [17].
2 http://www.cplusplus.com/reference/std/iterator/
7.2. Contenedores
[243]
Los iteradores tambin se pueden aplicar sobre flujos de E/S gracias a que la biblioteca
estndar proporciona cuatro tipos de iteradores enmarcados en el esquema general de
contenedores y algoritmos:
ostream_iterator, para escribir en un ostream.
istream_iterator, para leer de un istream.
ostreambuf_iterator, para escribir en un buffer de flujo.
istreambuf_iterator, para leer de un buffer de flujo.
El siguiente listado de cdigo muestra un ejemplo en el que se utiliza un iterador para
escribir sobre la salida estndar. Como se puede apreciar, el operador ++ sirve para desplazar el iterador de manera que sea posible llevar a cabo dos asignaciones consecutivas
sobre el propio flujo. De no ser as, el cdigo no sera portable.
Listado 7.12: Ejemplo de uso de iteradores para llevar a cabo operaciones de E/S.
1
#include <iostream>
#include <iterator>
3
4
5
6
int main () {
// Escritura de enteros en cout.
ostream_iterator<int> fs(cout);
7
8
9
*fs = 7;
++fs;
11
12
*fs = 6;
// Escribe 6.
10
13
return 0;
14
15
[244]
La otra opcin que se discutir brevemente en esta seccin es el proyecto Boost, cuyo
principal objetivo es extender la funcionalidad de STL. Gran parte de las bibliotecas del
proyecto Boost estn en proceso de estandarizacin con el objetivo de incluirse en el propio estndar de C++. Las principales caractersticas de Boost se resumen a continuacin:
Inclusin de funcionalidad no disponible en STL.
En ocasiones, Boost proporciona algunas alternativas de diseo e implementacin
respecto a STL.
Gestin de aspectos complejos, como por ejemplos los smart pointers.
Documentacin de gran calidad que tambin incluye discusiones sobre las decisiones de diseo tomadas.
Para llevar a cabo la instalacin de STLPort3 y Boost4 (includa la biblioteca para
manejo de grafos) en sistemas operativos Debian y derivados es necesarios ejecutar los
siguientes comandos:
$ sudo apt-get update
$ sudo apt-get install libstlport5.2-dev
$ sudo apt-get install libboost-dev
$ sudo apt-get install libboost-graph-dev
La figura 7.9 muestra un grafo dirigido que servir como base para la discusin del
siguiente fragmento de cdigo, en el cual se hace uso de la biblioteca Boost para llevar a
cabo el clculo de los caminos mnimos desde un vrtice al resto mediante el algoritmo
de Dijkstra.
Las bibliotecas de Boost se distribuyen bajo la Boost Software Licence, la cual permite el uso comercial y no-comercial.
3 http://www.stlport.org
4 http://www.boost.org
7.2. Contenedores
[245]
7
A
B
1
2
12
3
3
C
D
2
4
Figura 7.9: Representacin grfica del grafo de ejemplo utilizado para el clculo de caminos mnimos. La parte
derecha muestra la implementacin mediante listas de adyacencia planteada en la biblioteca Boost.
A continuacin se muestra un listado de cdigo (adaptado a partir de uno de los ejemplos de la biblioteca5 ) que permite la definicin bsica de un grafo dirigido y valorado.
El cdigo tambin incluye cmo hacer uso de funciones relevantes asociadas los grafos,
como el clculo de los cminos mnimos de un determinado nodo al resto.
Como se puede apreciar en el siguiente cdigo, es posible diferenciar cuatro bloques
bsicos. Los tres primeros se suelen repetir a la hora de manejar estructuras mediante la
biblioteca Boost Graph. En primer lugar, se lleva a cabo
la definicin de los tipos de datos
que se utilizarn, destacando
el
propio
grafo
(lneas
14-15 ) y los descriptores para manejar
tanto los vrtices (lnea 16 ) y las aristas (lneas 17-18 ). A continuacin se especifican los
elementos concretos del grafo, es decir, las etiquetas textuales asociadas a los vrtices,
las aristas
o arcos y los pesos de las mismas. Finalmente, ya es posible instanciar el grafo
(lnea 32 ), el descriptor usado para especificar el clculo de caminos
mnimos desde A
5 http://www.boost.org/doc/libs/1_55_0/libs/graph/example/dijkstra-example.cpp
[247]
Al ejecutar, la salida del programa mostrar todos los caminos mnimos desde el vrtice especificado; en este caso, desde el vrtice A.
Distancias y nodos padre:
Distancia(A)
Distancia(B)
Distancia(C)
Distancia(D)
Distancia(E)
7.3.
=
=
=
=
=
0,
9,
2,
4,
7,
Padre(A)
Padre(B)
Padre(C)
Padre(D)
Padre(E)
=
=
=
=
=
A
E
A
C
D
Al igual que ocurre con otros elementos transversales a la arquitectura de un motor de juegos, como por
ejemplos los contenedores de datos (ver captulo 5), las
cadenas de texto se utilizan extensivamente en cualquier
proyecto software vinculado al desarrollo de videojuegos.
Aunque en un principio pueda parecer que la utilizacin y la gestin de cadenas de texto es una
cuestin trivial, en realidad existe una gran variedad de tcnicas y restricciones vinculadas a este tipo de datos. Su consideracin puede ser muy relevante a la hora de mejorar el rendimiento de la aplicacin.
cad
'H'
p
'o'
'l'
'a'
'm'
Uno de los aspectos crticos a la hora de tratar con cadenas de texto est vinculado con el almacenamiento y
gestin de las mismas. Si se considera el hecho de utilizar
C/C++ para desarrollar un videojuego, entonces es importante no olvidar que en estos lenguajes de programacin
las cadenas de texto no son un tipo de datos atmica, ya
que se implementan mediante un array de caracteres.
'u'
'n'
'd'
'o'
Figura 7.10: Esquema grfico de la
implementacin de una cadena mediante un array de caracteres.
[248]
[249]
Profiling strings!
El uso de herramientas de profiling para evaluar el impacto del uso y gestin
de una implementacin concreta de cadenas de texto puede ser esencial para
mejorar el frame rate del juego.
[250]
7 Se recomienda la visualizacin del curso Introduction to Algorithms del MIT, en concreto las clases 7 y 8,
disponible en la web.
[251]
7.4.
Tricks. En muchos juegos es bastante comn encontrar trucos que permiten modificar significativamente algunos aspectos internos del juego, como por ejemplo la capacidad de desplazamiento del personaje principal. Tradicionalmente, la activacin
de trucos se ha realizado mediante combinaciones de teclas.
[252]
Dentro del mbito de los ficheros de configuracin, los archivos XML representan
otra posibilidad para establecer los parmetros de un motor de juegos. En este caso, es
necesario llevar a cabo un proceso de parsing para obtener la informacin asociada a
dichos parmetros.
El lenguaje XML. eXtensible Markup Language es un metalenguaje extensible basado en etiquetas que permite la definicin de lenguajes especficos de un determinado dominio. Debido a su popularidad, existen multitud de herramientas que permiten
el tratamiento y la generacin de archivos bajo este formato.
Tradicionalmente, las plataformas de juego que sufran restricciones de memoria, debido a limitaciones en su capacidad, hacan uso de un formato binario para almacenar
la informacin asociada a la configuracin. Tpicamente, dicha informacin se almacenaba en tarjetas de memoria externas que permitan salvar las partidas guardadas y la
configuracin proporcionada por el usuario de un determinado juego.
Este planteamiento tiene como principal ventaja la eficiencia en el almacenamiento de
informacin. Actualmente, y debido en parte a la convergencia de la consola de sobremesa
a estaciones de juegos con servicios ms generales, la limitacin de almacenamiento en
memoria permanente no es un problema, normalmente. De hecho, la mayora de consolas
de nueva generacin vienen con discos duros integrados.
Los propios registros del sistema operativo tambin se pueden utilizar como soporte
al almacenamiento de parmetros de configuracin. Por ejemplo, los sistemas operativos
de la familia de Microsoft WindowsTM proporcionan una base de datos global implementada mediante una estructura de rbol. Los nodos internos de dicha estructura actan como
directorios mientras que los nodos hoja representan pares clave-valor.
Tambin es posible utilizar la lnea de rdenes o incluso la definicin de variables de
entorno para llevar a cabo la configuracin de un motor de juegos.
Finalmente, es importante reflexionar sobre el futuro de los esquemas de almacenamiento. Actualmente, es bastante comn encontrar servicios que permitan el almacenamiento de partidas e incluso de preferencias del usuario en la red, haciendo uso de servidores en Internet normalmente gestionados por la propia compaa de juegos. Este tipo
de aproximaciones tienen la ventaja de la redundancia de datos, sacrificando el control de
los datos por parte del usuario.
[253]
(define simple-animation ()
(
3
4
(name
string)
(speed
float
:default 1.0)
(fade-in-seconds
float
:default 0.25)
(fade-out-seconds
float
:default 0.25)
8
9
10
11
12
(define-export anim-walk
13
(new simple-animation
14
:name "walk"
:speed 1.0
15
)
16
17
18
19
(define-export anim-walk-fast
(new simple-animation
20
:name "walk-fast"
21
:speed 2.0
22
)
23
24
[254]
7.5.
cdigo
datos
registros
[255]
archivos
cdigo
pila
registros
pila
datos
registros
pila
archivos
registros
pila
hilo
(a)
(b)
Figura 7.11: Esquema grfico de los modelos de programacin monohilo (a) y multihilo (b).
Eficacia, ya que tanto la creacin, el cambio de contexto, la destruccin y la liberacin de hilos es un orden de magnitud ms rpida que en el caso de los procesos
pesados. Recuerde que las operaciones ms costosas implican el manejo de operaciones de E/S. Por otra parte, el uso de este tipo de programacin en arquitecturas
de varios procesadores (o ncleos) incrementa enormemente el rendimiento de la
aplicacin.
[256]
SECCIN_ENTRADA
SECCIN_CRTICA
SECCIN_SALIDA
SECCIN_RESTANTE
Figura 7.12: Estructura general del cdigo vinculado a la seccin crtica.
7.6.
La biblioteca de hilos de I CE
[257]
El objetivo principal de esta biblioteca es abstraer al desarrollador de las capas inferiores e independizar el desarrollo de la aplicacin del sistema operativo y de la plataforma
hardware subyacentes. De este modo, la portabilidad de las aplicaciones desarrolladas
con esta biblioteca se garantiza, evitando posibles problemas de incompatibilidad entre
sistemas operativos.
[258]
Aplicacin cliente
Aplicacin servidora
Esqueleto
Proxy
API ICE
API ICE
Red de
comunicaciones
Adaptador de objetos
ICE runtime (servidor)
Figura 7.13: Arquitectura general de una aplicacin distribuida desarrollada con el middleware ZeroC I CE.
3
4
6
8
10
11
12
13
14
15
bool operator<
};
16
17
[259]
#ifndef __FILOSOFO__
#define __FILOSOFO__
#include <iostream>
#include <IceUtil/Thread.h>
#include <Palillo.h>
#define MAX_COMER 3
#define MAX_PENSAR 7
using namespace std;
class FilosofoThread : public IceUtil::Thread {
public:
FilosofoThread (const int& id, Palillo* izq, Palillo *der);
virtual void run ();
private:
void coger_palillos ();
void dejar_palillos ();
void comer () const;
void pensar () const;
int _id;
Palillo *_pIzq, *_pDer;
};
#endif
Cuando un filsofo piensa, entonces se abstrae del mundo y no se relaciona con ningn
otro filsofo. Cuando tiene hambre, entonces intenta coger a los palillos que tiene a su
izquierda y a su derecha (necesita ambos). Naturalmente, un filsofo no puede quitarle un
palillo a otro filsofo y slo puede comer cuando ha cogido los dos palillos. Cuando un
filsofo termina de comer, deja los palillos y se pone a pensar.
La solucin que se discutir en esta seccin se basa en implementar el filsofo como
un hilo independiente. Para ello, se crea la clase FilosofoThread que se expone en el
anterior listado de cdigo.
La implementacin de la funcin run() es trivial a partir de la descripcin del enunciado del problema.
Listado 7.19: La funcin FilosofoThread::run()
1
2
3
4
5
6
7
8
9
10
void
FilosofoThread::run ()
{
while (true) {
coger_palillos();
comer();
dejar_palillos();
pensar();
}
}
[260]
Antes de abordar esta problemtica, se mostrar cmo lanzar los hilos que representan a los cinco filsofos. El siguiente listado de cdigo muestra el cdigo bsico necesario para lanzar
los filsofos (hilos). Note cmo en la lnea 25 se llama a la funcin start() de Thread para comenzar
la ejecucin del mismo. Los objetos de tipo ThreadControl devueltos se almacenan en un vector para, posteriormente, unir los hilos creados. Para ello,
se hace uso de la funcin join() de la clase ThreadControl,
tal y como se muestra en la lnea
#include
#include
#include
#include
<IceUtil/Thread.h>
<vector>
<Palillo.h>
<Filosofo.h>
#define NUM 5
int main (int argc, char *argv[]) {
std::vector<Palillo*> palillos;
std::vector<IceUtil::ThreadControl> threads;
int i;
// Se instancian los palillos.
for (i = 0; i < NUM; i++)
palillos.push_back(new Palillo);
// Se instancian los filsofos.
for (i = 0; i < NUM; i++) {
// Cada filsofo conoce los palillos
// que tiene a su izda y derecha.
IceUtil::ThreadPtr t =
new FilosofoThread(i, palillos[i], palillos[(i + 1) % NUM]);
// start sobre hilo devuelve un objeto ThreadControl.
threads.push_back(t->start());
}
// Unin de los hilos creados.
std::vector<IceUtil::ThreadControl>::iterator it;
for (it = threads.begin(); it != threads.end(); ++it)
it->join();
return 0;
}
[261]
lock(), que intenta adquirir el cerrojo. Si ste ya estaba cerrado, entonces el hilo
que invoc a la funcin se suspende hasta que el cerrojo quede libre. La llamada a
dicha funcin retorna cuando el hilo ha adquirido el cerrojo.
tryLock(), que intenta adquirir el cerrojo. A diferencia de lock(), si el cerrojo est
cerrado, la funcin devuelve false. En caso contrario, devuelve true con el cerrojo
cerrado.
unlock(), que libera el cerrojo.
class Mutex {
public:
Mutex ();
Mutex (MutexProtocol p);
~Mutex ();
void lock
() const;
bool tryLock () const;
void unlock () const;
typedef LockT<Mutex> Lock;
typedef TryLockT<Mutex> TryLock;
};
#ifndef __PALILLO__
#define __PALILLO__
#include <IceUtil/Mutex.h>
class Palillo : public IceUtil::Mutex {
};
#endif
[262]
Para que los filsofos puedan utilizar los palillos, habr que utilizar la funcionalidad previamente discutida, es decir, las funciones lock() y unlock(), en las funciones coger_palillos() y dejar_palillos(). La solucin planteada garantiza que no se ejecutarn dos
llamadas a lock() sobre un palillo por parte de un mismo hilo, ni tampoco una llamada sobre unlock() si previamente no se adquiri el palillo.
Listado 7.23: Acceso concurrente a los palillos
1
2
3
4
5
6
7
8
9
10
11
12
13
void
FilosofoThread::coger_palillos ()
{
_pIzq->lock();
_pDer->lock();
}
void
FilosofoThread::dejar_palillos ()
{
_pIzq->unlock();
_pDer->unlock();
}
La solucin planteada es poco flexible debido a que los filsofos estn inactivos durante el periodo de tiempo que pasa desde que dejan de pensar hasta que cogen los dos
palillos. Una posible variacin a la solucin planteada hubiera sido continuar pensando
(al menos hasta un nmero mximo de ocasiones) si los palillos estn ocupados. En esta
variacin se podra utilizar la funcin tryLock() para modelar dicha problemtica.
Riesgo de interbloqueo. Si todos los filsofos cogen al mismo tiempo el palillo que
est a su izquierda se producir un interbloqueo ya que la solucin planteada no
podra avanzar hasta que un filsofo coja ambos palillos.
Evitando interbloqueos
El uso de las funciones lock() y unlock() puede generar problemas importantes si, por
alguna situacin no controlada, un cerrojo previamente adquirido con lock() no se libera
posteriormente con una llamada a unlock(). El siguiente listado de cdigo muestra esta
problemtica.
[263]
#include <IceUtil/Mutex.h>
2
3
class Test {
public:
void mi_funcion () {
_mutex.lock();
5
6
7
9
}
10
11
12
private:
IceUtil::Mutex _mutex;
13
14
};
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <IceUtil/Mutex.h>
class Test {
public:
void mi_funcion () {
IceUtil::Mutex::Lock lock(_mutex);
for (int i = 0; i < 5; i++)
if (i == 3) return; // Ningn problema...
} // El destructor de lock libera el cerrojo.
private:
IceUtil::Mutex _mutex;
};
No olvides...
Liberar un cerrojo slo si fue previamente adquirido y llamar a unlock()
tantas veces como a lock() para que el
cerrojo quede disponible para otro hilo.
[264]
Sin embargo, existe una diferencia fundamental entre ambas. Internamente, el cerrojo
recursivo est implementado con un contador inicializado a cero. Cada llamada a lock()
incrementa el contador, mientras que cada llamada a unlock() lo decrementa. El cerrojo
estar disponible para otro hilo cuando el contador alcance el valor de cero.
Listado 7.26: La clase IceUtil::RecMutex
1
2
3
4
5
6
7
8
9
10
11
12
13
class RecMutex {
public:
RecMutex ();
RecMutex (MutexProtocol p);
~RecMutex ();
void lock
() const;
bool tryLock () const;
void unlock () const;
typedef LockT<RecMutex> Lock;
typedef TryLockT<RecMutex> TryLock;
};
[265]
Datos
compartidos
Condiciones
x
y
Operaciones
Cdigo
inicializacin
Figura 7.15: Representacin grfica del concepto de monitor.
Listado 7.27: La clase IceUtil::Monitor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wait () const;
timedWait (const Time&) const;
notify ();
notifyAll ();
[266]
[267]
: Consumidor
: Queue
: Productor
get ()
wait ()
notify
put ()
return item
#include <IceUtil/Monitor.h>
#include <IceUtil/Mutex.h>
#include <deque>
4
5
6
7
template<class T>
public:
10
11
12
notify();
13
14
15
16
17
18
wait();
T item = _queue.front();
_queue.pop_front();
19
20
21
return item;
22
23
24
25
26
27
private:
deque<T> _queue; // Cola de doble entrada.
};
(lneas 18-19) hasta que otro hilo que ejecute put() realice la invocacin sobre notify() (lnea 13 ).
[268]
Recuerde que para que un hilo bloqueado por wait() reanude su ejecucin, otro hilo
ha de ejecutar notify() y liberar el monitor mediante unlock(). Sin embargo, en el anterior
listado de cdigo no existe ninguna llamada explcita a unlock(). Es incorrecta la solucin? La respuesta es no, ya que la liberacin del monitor se delega en Lock cuando la
funcin put() finaliza, es decir, justo despus de ejecutar la operacin notify() en este caso
particular.
No olvides... Usar Lock y TryLock para evitar posibles interbloqueos causados por la
generacin de alguna excepcin o una terminacin de la funcin no prevista inicialmente.
Volviendo al ejemplo anterior, considere dos hilos distintos que interactan con la estructura creada, de manera genrica, para almacenar los slots que permitirn la activacin
de habilidades especiales por parte del jugador virtual. Por ejemplo, el hilo asociado al
productor podra implementarse como se muestra en el siguiente listado.
public:
3
4
5
6
void run () {
for (int i = 0; i < 5; i++) {
IceUtil::ThreadControl::sleep
(IceUtil::Time::seconds(rand() % 7));
_queue->put("TestSlot");
9
10
11
12
13
14
15
16
private:
Queue<string> *_queue;
};
Suponiendo que el cdigo del consumidor sigue la misma estructura, pero extrayendo elementos de la estructura de datos compartida, entonces sera posible lanzar distintos
hilos para comprobar que el acceso concurrente sobre los distintos slots se realiza de
manera adecuada. Adems, sera sencillo visualizar, mediante mensajes por la salida estndar, que efectivamente los hilos consumidores se suspenden en wait() hasta que hay al
menos algn elemento en la estructura de datos compartida con los productores.
Antes de pasar a discutir en la seccin 7.9 un caso de estudio para llevar a cabo el
procesamiento de alguna tarea en segundo plano, resulta importante volver a discutir la
solucin planteada inicialmente para el manejo de monitores. En particular, la implementacin de las funciones miembro put() y get() puede generar sobrecarga debido a que,
cada vez que se aade un nuevo elemento a la estructura de datos, se realiza una invocacin. Si no existen hilos esperando, la notificacin se pierde. Aunque este hecho no
conlleva ningn efecto no deseado, puede generar una reduccin del rendimiento si el
nmero de notificaciones se dispara.
[269]
#include <IceUtil/Monitor.h>
#include <IceUtil/Mutex.h>
#include <deque>
using namespace std;
template<class T>
class Queue : public IceUtil::Monitor<IceUtil::Mutex> {
public:
Queue () : _consumidoresEsperando(0) {}
void put (const T& item) { // Aade un nuevo item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
_queue.push_back(item);
if (_consumidoresEsperando) notify();
}
T get () { // Consume un item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
while (_queue.size() == 0) {
try {
_consumidoresEsperando++;
wait();
_consumidoresEsperando--;
}
catch (...) {
_consumidoresEsperando--;
throw;
}
}
T item = _queue.front();
_queue.pop_front();
return item;
}
private:
deque<T> _queue; // Cola de doble entrada.
int _consumidoresEsperando;
};
[270]
7.7.
Concurrencia en C++11
#include <iostream>
#include <thread>
4
5
6
7
8
9
int main() {
thread th(&func, 100);
10
11
th.join();
12
13
14
Para poder compilar este programa con la ltima versin del estndar puede utilizar
el siguiente comando:
$ g++ -std=c++11 Thread_c++11.cpp -o Thread -pthread
int contador = 0;
mutex contador_mutex;
void anyadir_doble (int x) {
int tmp = 2 * x;
contador_mutex.lock();
contador += tmp;
contador_mutex.unlock();
}
8 http://es.cppreference.com/w/cpp/thread
9 http://es.cppreference.com/w/cpp/thread/mutex
[271]
#include <atomic>
atomic<int> contador(0);
void anyadir_doble (int x) {
contador += 2 * x;
}
class Palillo {
public:
mutex _mutex;
};
A continuacin se muestra la funcin comer(), la cual incluye la funcionalidad asociada a coger los palillos. Note cmo esta funcin lambda o funcin annima se define
dentro del propio cdigo de la funcin main (en lnea) y propicia la generacin de un
cdigo ms claro y directo. Adems, el uso de auto oculta la complejidad asociada al
manejo de punteros a funciones.
Esta funcin recibe como argumentos (lneas 4-5 ) apuntadores a dos palillos, junto a
sus identificadores, y el identificador del filsofo que va a intentar cogerlos para comer.
En primer
lugar, es interesante resaltar el uso de std::lock sobre los propios palillos en
la lnea 7 para evitar el problema clsico del interbloqueo de los filsofos comensales.
Esta funcin hace uso de un algoritmo de evasin del interbloqueo y permite adquirir dos
o ms mutex mediante
nica instruccin. As mismo, tambin se utiliza el wrapper
una
lock_guard (lneas 12 y 17 ) con el objetivo de indicar de manera explcita que los mutex
han sido adquiridos y que stos deben adoptar el propietario de dicha adquisicin.
El resto del cdigo de esta funcin muestra por la salida estndar mediante std::cout
mensajes que indican el filsofo ha adquirido los palillos.
Una vez definida la funcionalidad general asociada a un filsofo, el siguiente paso
consiste en instanciar los palillos a los propios filsofos.
[272]
int main () {
/* Funcin lambda (funcin annima) */
/* Adis a los punteros a funciones con auto */
auto comer = [](Palillo* pIzquierdo, Palillo* pDerecho,
int id_filosofo, int id_pIzquierdo, int id_pDerecho) {
/* Para evitar interbloqueos */
lock(pIzquierdo->_mutex, pDerecho->_mutex);
/* Wrapper para adquirir el mutex en un bloque de cdigo */
/* Indica que el mutex ha sido adquirido y que debe
adoptar al propietario del cierre */
lock_guard<mutex> izquierdo(pIzquierdo->_mutex, adopt_lock);
string si = "\tFilsofo " + to_string(id_filosofo) +
" cogi el palillo " + to_string(id_pIzquierdo) + ".\n";
cout << si.c_str();
lock_guard<mutex> derecho(pDerecho->_mutex, adopt_lock);
string sd = "\tFilsofo " + to_string(id_filosofo) +
" cogi el palillo " + to_string(id_pDerecho) + ".\n";
cout << sd.c_str();
string pe = "Filsofo " + to_string(id_filosofo) + " come.\n";
cout << pe;
std::chrono::milliseconds espera(1250);
std::this_thread::sleep_for(espera);
/* Los mutex se desbloquean al salir de la funcin */
};
lnea 2 , el cual se rellena mediante un bucle for a partir de la lnea 20 . Sin embargo,
antes se inserta
el filsofo que tiene a su derecha el primer palillo y a su izquierda el
ltimo (lneas 6-17 ).
[273]
clase thread (lnea 21 ). Cada instancia de esta clase maneja un apuntador a la funcin
comer, la cual recibe como argumentos los apuntadores a los palillos que correspondan
a cada filsofo y los identificadores correspondientes para mostrar por la salida estndar
los mensajes reflejan la evolucin de la simulacin.
La ltima parte del cdigo, en las lneas 32-34 , lleva a cabo la ejecucin efectiva de
los hilos y la espera a la finalizacin de los mismos mediante la funcin thread::join().
Listado 7.37: Filsofos comensales en C++11 (Creacin filsofos)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* Vector de filsofos */
vector<thread> filosofos(numero_filosofos);
/* Primer filsofo */
/* A su derecha el palillo 1 y a su izquierda el palillo 5 */
filosofos[0] = thread(comer,
/* Palillo derecho (1) */
palillos[0].get(),
/* Palillo izquierdo (5) */
palillos[numero_filosofos - 1].get(),
/* Id filsofo */
1,
/* Id palillo derecho */
1,
/* Id palillo izquierdo */
numero_filosofos
);
/* Restos de filsofos */
for (int i = 1; i < numero_filosofos; i++) {
filosofos[i] = (thread(comer,
palillos[i - 1].get(),
palillos[i].get(),
i + 1,
i,
i + 1
)
);
}
/* A comer... */
for_each(filosofos.begin(),
filosofos.end(),
mem_fn(&thread::join));
[274]
Sin embargo, s que es posible delegar en un segundo plano aspectos tradicionalmente independientes de la
parte grfica, como por ejemplo el mdulo de Inteligencia Artificial o la carga de recursos. En esta seccin, se
discutirn las caractersticas bsicas que Ogre3D ofrece
para la carga de recursos en segundo plano.
En primer lugar, puede ser necesario compilar Ogre
con soporte para multi-threading, tpicamente soportado
por la biblioteca boost. Para ejemplificar este problema, a
continuacin se detallan las instrucciones para compilar
Figura 7.18: Ogre proporciona me- el cdigo fuente de Ogre 1.8.110 .
canismos para llevar a cabo la carga
Antes de nada, es necesario asegurarse de que la bide recursos en segundo plano.
blioteca de hilos de boost est instalada y es recomendable utilizar cmake para generar el Makefile que se utilizar para compilar el cdigo fuente. Para ello, en sistemas operativos Debian GNU/Linux y
derivados, hay que ejecutar los siguientes comandos:
$ sudo apt-get install libboost-thread-dev
$ sudo apt-get install cmake cmake-qt-gui
El siguiente paso consiste en ejecutar la interfaz grfica que nos ofrece cmake para
hacer explcito el soporte multi-hilo de Ogre. Para ello, simplemente hay que ejecutar el
comando cmake-gui y, a continuacin, especificar la ruta en la que se encuentra el cdigo
fuente de Ogre y la ruta en la que se generar el resultado de la compilacin, como se
muestra en la figura 7.19. Despus, hay que realizar las siguientes acciones:
1. Hacer click en el botn Configure.
2. Ajustar el parmetro OGRE_CONFIG_THREADS11 .
3. Hacer click de nuevo en el botn Configure.
4. Hacer click en el botn Generate para generar el archivo Makefile.
Es importante recalcar que el parmetro OGRE_CONFIG_THREADS establece el
tipo de soporte de Ogre respecto al modelo de hilos. Un valor de 0 desabilita el soporte
de hilos. Un valor de 1 habilita la carga completa de recursos en segundo plano. Un valor
de 2 activa solamente la preparacin de recursos en segundo plano12 .
Finalmente, tan slo es necesario compilar y esperar a la generacin de todos los ejecutables y las bibliotecas de Ogre. Note que es posible optimizar este proceso indicando
el nmero de procesadores fsicos de la mquina mediante la opcin j de make.
$ cd ogre_build_v1-8-1
$ make -j 2
10 http://www.ogre3d.org/download/source
11 http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Building+Ogre+With+CMake
12 Segn los desarrolladores de Ogre, slo se recomienda utilizar un valor de 0 o de 2 en sistemas GNU/Linux
[275]
Figura 7.19: Generando el fichero Makefile mediante cmake para compilar Ogre 1.8.1.
Para ejemplificar la carga de recursos en segundo plano, se retomar el ejemplo discutido en la seccin 6.2.2. En concreto, el siguiente listado de cdigo muestra cmo se ha
modificado la carga de un track de msica para explicitar que se haga en segundo plano.
Como se puede apreciar, en la lnea 13 se hace uso de la funcin setBackgroundLoaded() con el objetivo de indicar que el recurso se cargar en segundo plano.
A continuacin, en la siguiente lnea, se aade un objeto de la clase MyListener, la
cual deriva a su vez de la clase Ogre::Resource::Listener para controlar la notificacin
de la carga del recurso en segundo plano.
Callbacks. El uso de funciones de retrollamada permite especificar el cdigo que se
ejecutar cuando haya finalizado la carga de un recurso en segundo plano. A partir
de entonces, ser posible utilizarlo de manera normal.
En este ejemplo se ha sobreescrito la funcin backgroundLoadingComplete() que permite conocer cundo se ha completado la carga en segundo plano del recurso. La documentacin relativa a la clase Ogre::Resource::Listener13 discute el uso de otras funciones
de retrollamada que resultan tiles para cargar contenido en segundo plano.
13 http://www.ogre3d.org/docs/api/1.9/class_ogre_1_1_resource.html
[276]
TrackPtr
TrackManager::load
(const Ogre::String& name, const Ogre::String& group)
{
// Obtencin del recurso por nombre...
TrackPtr trackPtr = getByName(name);
// Si no ha sido creado, se crea.
if (trackPtr.isNull())
trackPtr = create(name, group);
// Carga en segundo plano y listener.
trackPtr->setBackgroundLoaded(true);
trackPtr->addListener(new MyListener);
// Escala la carga del recurso en segundo plano.
if (trackPtr->isBackgroundLoaded())
trackPtr->escalateLoading();
else
trackPtr->load();
return trackPtr;
}
#ifndef __MYLISTENERH__
#define __MYLISTENERH__
3
4
#include <OGRE/OgreResource.h>
#include <iostream>
6
7
8
9
10
12
13
11
14
15
};
16
17
#endif
[278]
#ifndef __AI__
#define __AI__
#include <IceUtil/IceUtil.h>
#include <vector>
class AIThread : public IceUtil::Thread {
public:
AIThread (const IceUtil::Time& delay);
int getColorAt (const int& index) const;
void reset ();
void update ();
virtual void run ();
private:
IceUtil::Time _delay; // Tiempo entre actualizaciones.
IceUtil::Mutex _mutex; // Cerrojo para acceso exclusivo.
std::vector<int> _seq; // Secuencia de colores.
};
typedef IceUtil::Handle<AIThread> AIThreadPtr; // Smart pointer.
#endif
[280]
int MyApp::start() {
_root = new Ogre::Root();
if(!_root->restoreConfig())
{
_root->showConfigDialog();
_root->saveConfig();
}
_AI = new AIThread(IceUtil::Time::seconds(1));
IceUtil::ThreadControl tc = _AI->start();
// Se desliga del hilo principal.
tc.detach();
// ...
}
Rendimiento con hilos. El uso de hilos implica cambios de contexto y otras operaciones a nivel de sistema operativo que consumen miles de ciclos de ejecucin.
Evale siempre el impacto de usar una solucin multi-hilo.
[282]
Captulo 7 :: Bibliografa