Documente Academic
Documente Profesional
Documente Cultură
Ttulo:
Desarrollo de Videojuegos: Un Enfoque Prctico
Edicin: Julio 2014
Autores: David Vallejo Fernndez, Carlos Gonzlez Morcillo, David
Villa Alises, Francisco Jurado Monroy, Francisco Moya
Fernndez, Javier Alonso Albusac Jimnez, Cleto Martn
Angelina, Sergio Prez Camacho, Flix Jess Villanueva
Molina, Csar Mora Castro, Jos Jess Castro Snchez,
Miguel ngel Redondo Duque, Luis Jimnez Linares,
Jorge Lpez Gonzlez, Miguel Garca Corchero, Manuel
Palomo Duarte, Guillermo Simmross Wattenberg, Jos Luis
Gonzlez Snchez. Colaboradores: David Frutos Talavera,
Sergio Fernndez Durn
ISBN:
978-84-942382-9-1
Depsito Legal: VG 415-2014
Publica: EdLibrix
Edita:
David Vallejo, Carlos Gonzlez y David Villa
Portada: (Ilustracin de portada) Vctor Barba Pizarro
Diseo: Carlos Gonzlez Morcillo y Vctor Barba Pizarro
Este libro fue compuesto con LaTeX a partir de una plantilla de David Villa Alises
y Carlos Gonzlez Morcillo.
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
La industria del videojuego ocupa el primer lugar en el ocio audio-visual e interactivo a nivel mundial, por encima de industrias tan potentes como el cine o la msica.
Como consecuencia directa, existe una gran demanda de profesionales cualicados
para disear y desarrollar videojuegos no slo para consolas y ordenadores, sino tambin para el ms que creciente mercado de los telfonos mviles.
Este libro, dividido en cuatro bloques, tiene como objetivo principal proporcionar
los conocimientos necesarios para llevar a cabo dicha tarea desde una perspectiva
esencialmente tcnica:
1. Arquitectura del Motor, donde se estudian los aspectos esenciales del diseo
de un motor de videojuegos, as como las tcnicas bsicas de programacin
y patrones de diseo. En este bloque tambin se estudian los conceptos ms
relevantes del lenguaje de programacin C++.
2. Programacin Grca, donde se presta especial atencin a los algoritmos y
tcnicas de representacin grca, junto con las optimizaciones en sistemas de
despliegue interactivo.
3. Tcnicas Avanzadas, donde se recogen ciertos aspectos avanzados, como estructuras de datos especcas, tcnicas de validacin y pruebas o simulacin
fsica. As mismo, en este bloque se profundiza en el lenguaje C++.
4. Desarrollo de Componentes, donde, nalmente, se detallan ciertos componentes especcos del motor, como la Inteligencia Articial, Networking, Sonido y
Multimedia o tcnicas avanzadas de Interaccin.
Requisitos previos
Este libro tiene un pblico objetivo con un perl principalmente tcnico. Al igual
que el curso del que surgi, est orientado a la capacitacin de profesionales de la
programacin de videojuegos. De esta forma, este libro no est orientado para un
pblico de perl 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
y 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. 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 tres
primeras 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.
De igual modo, se quiere reejar el agradecimiento especial al personal de administracin y servicios de la Escuela Superior de Informtica, por su soporte, predisposicin y ayuda en todos los caprichosos requisitos que plantebamos a lo largo del
curso.
Por otra parte, este agradecimiento tambin se hace extensivo a la Escuela de Informatica de Ciudad Real y al Departamento de Tecnologas y Sistema de Informacin
de la Universidad de Castilla-La Mancha.
Finalmente, los autores desean agradecer su participacin a los colaboradores de
las tres primeras ediciones: Indra Software Labs, la asociacin de desarrolladores de
videojuegos Stratos, Libro Virtual, Devilish Games, Dolores Entertainment, From the
Bench, Iberlynx, KitMaker, Playspace, Totemcat/Materia Works y ZuinqStudio.
Autores
ndice general
1. Introduccin
1
3
10
15
15
16
17
18
18
19
22
22
23
23
24
1.2.12. Audio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
26
2. Herramientas de Desarrollo
I
27
[II]
NDICE GENERAL
2.1. Introduccin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
28
28
31
31
2.2.4. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
39
39
45
50
50
2.3.2. Documentacin . . . . . . . . . . . . . . . . . . . . . . . . .
59
62
65
65
65
66
67
68
71
3.2. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
74
74
77
81
84
3.3.1. Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . .
84
86
90
3.4. Plantillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
94
94
96
98
99
99
[III]
4. Patrones de Diseo
107
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
147
[IV]
NDICE GENERAL
5.3.3. List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
171
219
[V]
7.3.1. Cuestiones especcas . . . . . . . . . . . . . . . . . . . . . 235
7.3.2. Optimizando el tratamiento de cadenas . . . . . . . . . . . . 236
7.3.3. Hashing de cadenas . . . . . . . . . . . . . . . . . . . . . . . 238
7.4. Conguracin del motor . . . . . . . . . . . . . . . . . . . . . . . . 239
7.4.1. Esquemas tpicos de conguracin . . . . . . . . . . . . . . . 239
7.4.2. Caso de estudio. Esquemas de denicin. . . . . . . . . . . . 240
7.5. Fundamentos bsicos de concurrencia . . . . . . . . . . . . . . . . . 241
7.5.1. El concepto de hilo . . . . . . . . . . . . . . . . . . . . . . . 241
7.5.2. El problema de la seccin crtica . . . . . . . . . . . . . . . . 242
7.6. La biblioteca de hilos de I CE . . . . . . . . . . . . . . . . . . . . . . 244
7.6.1. Internet Communication Engine . . . . . . . . . . . . . . . . 244
7.6.2. Manejo de hilos . . . . . . . . . . . . . . . . . . . . . . . . . 245
7.6.3. Exclusin mutua bsica . . . . . . . . . . . . . . . . . . . . . 247
7.6.4. Flexibilizando el concepto de mutex . . . . . . . . . . . . . . 251
7.6.5. Introduciendo monitores . . . . . . . . . . . . . . . . . . . . 251
7.7. Concurrencia en C++11 . . . . . . . . . . . . . . . . . . . . . . . . . 256
7.7.1. Filsofos comensales en C++11 . . . . . . . . . . . . . . . . 258
7.8. Multi-threading en Ogre3D . . . . . . . . . . . . . . . . . . . . . . . 260
7.9. Caso de estudio. Procesamiento en segundo plano mediante hilos . . . 263
II
Programacin Grca
267
269
[VI]
NDICE GENERAL
295
313
321
[VII]
11.3. Procesamiento de Recursos Grcos . . . . . . . . . . . . . . . . . . 346
11.3.1. Ejemplo de uso . . . . . . . . . . . . . . . . . . . . . . . . . 348
11.4. Gestin de Recursos y Escena . . . . . . . . . . . . . . . . . . . . . 351
11.4.1. Recursos empaquetados . . . . . . . . . . . . . . . . . . . . 352
11.4.2. Gestin del ratn . . . . . . . . . . . . . . . . . . . . . . . . 352
11.4.3. Geometra Esttica . . . . . . . . . . . . . . . . . . . . . . . 353
11.4.4. Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
11.4.5. Mscaras . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
12. APIS de Grcos 3D
359
369
383
[VIII]
NDICE GENERAL
14.2.3. El Sistema de Dimensin Unicado . . . . . . . . . . . . . . 390
14.2.4. Deteccin de eventos de entrada . . . . . . . . . . . . . . . . 391
409
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
429
[IX]
16.2. Tipos de Fuentes de Luz . . . . . . . . . . . . . . . . . . . . . . . . 430
16.3. Sombras Estticas Vs Dinmicas . . . . . . . . . . . . . . . . . . . . 431
16.3.1. Sombras basadas en Stencil Buffer . . . . . . . . . . . . . . . 432
16.3.2. Sombras basadas en Texturas . . . . . . . . . . . . . . . . . . 434
16.4. Ejemplo de uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
16.5. Mapas de Iluminacin . . . . . . . . . . . . . . . . . . . . . . . . . . 437
16.6. Ambient Occlusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
16.7. Radiosidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440
17. Exportacin y Uso de Datos de Intercambio
445
451
465
[X]
NDICE GENERAL
19.9. TriangleMeshCollisionShape . . . . . . . . . . . . . . . . . . . . . . 493
19.10.Deteccin de colisiones . . . . . . . . . . . . . . . . . . . . . . . . . 494
19.11.Restriccin de Vehculo . . . . . . . . . . . . . . . . . . . . . . . . . 496
19.12.Determinismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
19.13.Escala de los Objetos . . . . . . . . . . . . . . . . . . . . . . . . . . 502
19.14.Serializacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
III
505
507
527
[XI]
21.3.2. Predicados . . . . . . . . . . . . . . . . . . . . . . . . . . . 571
21.3.3. Functors
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 572
. . . . . . . . . . . . . . . . . . . . . . 593
635
663
[XII]
NDICE GENERAL
687
703
[XIII]
25.2.5. Parches: adaptacin a Debian . . . . . . . . . . . . . . . . . 721
25.2.6. Actualizacin del paquete . . . . . . . . . . . . . . . . . . . 724
25.2.7. Subir un paquete a Debian . . . . . . . . . . . . . . . . . . . 725
25.3. Otros formatos de paquete . . . . . . . . . . . . . . . . . . . . . . . 725
26. Representacin Avanzada
727
[XIV]
NDICE GENERAL
26.7.4. Tcnicas y Algoritmos . . . . . . . . . . . . . . . . . . . . . 784
26.7.5. Exteriores y LOD en OGRE . . . . . . . . . . . . . . . . . . 789
26.7.6. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . 798
799
IV
Desarrollo de Componentes
821
823
[XV]
28.3. Algoritmos de bsqueda . . . . . . . . . . . . . . . . . . . . . . . . 864
28.3.1. Problemas y soluciones. . . . . . . . . . . . . . . . . . . . . 864
28.3.2. Organizar la bsqueda de soluciones . . . . . . . . . . . . . . 867
28.3.3. Bsqueda con informacin . . . . . . . . . . . . . . . . . . . 871
28.4. Planicacin de caminos . . . . . . . . . . . . . . . . . . . . . . . . 876
28.4.1. Puntos visibles . . . . . . . . . . . . . . . . . . . . . . . . . 876
28.4.2. Problema de bsqueda en grafos . . . . . . . . . . . . . . . . 877
28.4.3. Obtencin del grafo de puntos visibles . . . . . . . . . . . . . 879
28.4.4. Aumentando la resolucin . . . . . . . . . . . . . . . . . . . 880
28.5. Diseo basado en agentes . . . . . . . . . . . . . . . . . . . . . . . . 883
28.5.1. Qu es un agente? . . . . . . . . . . . . . . . . . . . . . . . 883
28.5.2. Cmo se construye un agente? . . . . . . . . . . . . . . . . 885
28.5.3. Los comportamientos como gua de diseo . . . . . . . . . . 888
28.5.4. Implementacin de agentes basados en autmatas . . . . . . . 897
28.5.5. Usando los objetivos como gua de diseo . . . . . . . . . . . 909
28.5.6. Reexiones sobre el futuro . . . . . . . . . . . . . . . . . . . 912
28.6. Caso de estudio. Juego deportivo . . . . . . . . . . . . . . . . . . . . 913
28.6.1. Introduccin a Pygame . . . . . . . . . . . . . . . . . . . . . 914
28.6.2. Arquitectura propuesta . . . . . . . . . . . . . . . . . . . . . 917
28.6.3. Integracin de comportamientos bsicos . . . . . . . . . . . . 924
28.6.4. Diseo de la Inteligencia Articial . . . . . . . . . . . . . . . 928
28.6.5. Consideraciones nales . . . . . . . . . . . . . . . . . . . . . 929
28.7. Sistemas expertos basados en reglas . . . . . . . . . . . . . . . . . . 929
28.7.1. Introduccin . . . . . . . . . . . . . . . . . . . . . . . . . . 929
28.7.2. Caso de estudio: Stratego . . . . . . . . . . . . . . . . . . . . 931
29. Networking
937
[XVI]
NDICE GENERAL
981
998
[XVII]
30.6.2. Introduccin a GStreamer . . . . . . . . . . . . . . . . . . . 999
30.6.3. Algunos conceptos bsicos . . . . . . . . . . . . . . . . . . . 1000
30.6.4. GStreamer en Ogre . . . . . . . . . . . . . . . . . . . . . . . 1001
30.7. Comentarios nales sobre vdeo digital . . . . . . . . . . . . . . . . . 1005
31. Interfaces de Usuario Avanzadas
1007
[XVIII]
NDICE GENERAL
31.18.2.Bucle Principal . . . . . . . . . . . . . . . . . . . . . . . . . 1056
31.18.3.Finalizacin y funcin Main . . . . . . . . . . . . . . . . . . 1057
1079
A. Introduccin a OgreFramework
1081
1093
[XIX]
B.2.3. Manchas en la cmara . . . . . . . . . . . . . . . . . . . . . 1104
B.3. Luces en Vault Defense: efecto fuego . . . . . . . . . . . . . . . . . 1106
B.4. Enemigos en Vault Defense . . . . . . . . . . . . . . . . . . . . . . . 1107
Listado de acrnimos
ACE
AMI
API
APT
ARM
AVI
AVL
B-R EP
Boundary Representation
BBT
BDI
BIK
BINK Video
BMP
BitMaP
BRDF
BSD
BSP
BTT
CAD
CCD
Charge-Coupled Device
CD
Compact Disc
CEGUI
CMOS
Complementary Metal-Oxide-Semiconductor
CMYK
CORBA
CPU
CRTP
CSG
CUDA
CVS
DBTS
DCU
[XXII]
ACRNIMOS
DD
Debian Developer
DEB
DEHS
DFSG
DIP
DLL
DOD
Diamond Of Death
DOM
DTD
DVB
DVD
E/S
Entrada/Salida
EASTL
EA
Electronic Arts
ELF
FIFO
FLAC
FPS
FSM
GCC
GDB
GNU Debugger
GDD
GIMP
GLUT
GLU
OpenGL Utility
GNOME
GNU
GPL
GPS
GPU
GPV
GTK
GIMP ToolKit
GUI
GUP
HDTV
HFSM
HOM
HSV
HTML
I/O
Input/Output
IANA
IA
Inteligencia Articial
IBM
IDL
[XXIII]
IEC
IEEE
IGC
In-Game Cinematics
IP
Internet Protocol
IPC
InterProcess Communication
IPO
InterPOlation curve
IPP
IPX
ITP
Intent to Package
ISO
ISP
I CE
JPG
KISS
LAN
LED
LGPL
LIFO
LOD
Level-Of-Detail
LSP
MAC
MAS
Multi-Agent Systems
MHI
MMOG
MMORPG
MP3
MPEG
MTU
MVC
NPC
Non-Player Character
NRVO
OCP
Open/Closed Principle
ODE
ODT
OpenDocument Text
OGI
OGRE
OIS
ONC
P2P
Peer To Peer
PDA
PHP
PMU
PNG
[XXIV]
ACRNIMOS
POO
POSIX
PPC
PowerPC
PUD
PVS
RAII
RFA
RFH
RFP
RGBA
RGB
RMI
ROAM
ROI
Region Of Interest
RPC
RPG
Role-Playing Games
RPM
RST
reStructuredText
RTP
RTS
Real-Time Strategy
RTTI
RTT
Render To Texture
RVO
SAX
SDK
SDL
SGBD
SGML
SGI
SIP
SLERP
SOLID
SRP
SRU
STL
SUT
SVCD
Super VCD
SVG
S LICE
TCP/IP
TCP
TDD
TDT
TGA
[XXV]
TIFF
TIF
TIFF
TLB
TTL
Time To Live
UCLM
UDP
UML
URL
USB
UTF
UUID
VCD
Video CD
VCS
VGA
VRML
WAV
WAVeform
WNPP
XDR
XML
YAGNI
YAML
Captulo
Introduccin
David Vallejo Fernndez
ctualmente, la industria del videojuego goza de una muy buena salud a nivel
mundial, rivalizando en presupuesto con las industrias cinematogrca y musical. En este captulo se discute, desde una perspectiva general, el desarrollo
de videojuegos, haciendo especial hincapi en su evolucin y en los distintos elementos involucrados en este complejo proceso de desarrollo. En la segunda parte del captulo se introduce el concepto de arquitectura del motor, como eje fundamental para
el diseo y desarrollo de videojuegos comerciales.
El primer videojuego
El videojuego Pong se considera
como unos de los primeros videojuegos de la historia. Desarrollado
por Atari en 1975, el juego iba incluido en la consola Atari Pong.
Se calcula que se vendieron unas
50.000 unidades.
1.1.
El desarrollo de videojuegos
1.1.1.
Lejos han quedado los das desde el desarrollo de los primeros videojuegos, caracterizados principalmente por su simplicidad y por el hecho de estar desarrollados
completamente sobre hardware. Debido a los distintos avances en el campo de la informtica, no slo a nivel de desarrollo software y capacidad hardware sino tambin
en la aplicacin de mtodos, tcnicas y algoritmos, la industria del videojuego ha evolucionado hasta llegar a cotas inimaginables, tanto a nivel de jugabilidad como de
calidad grca, tan slo hace unos aos.
[4]
CAPTULO 1. INTRODUCCIN
C1
1.1. El desarrollo de videojuegos
[5]
Finalmente, resulta especialmente importante destacar la existencia de motores de
juego (game engines), como por ejemplo Quake1 o Unreal2 , middlewares para el tratamiento de aspectos especcos de un juego, como por ejemplo la biblioteca Havok3
para el tratamiento de la fsica, o motores de renderizado, como por ejemplo Ogre 3D
[52]. Este tipo de herramientas, junto con tcnicas especcas de desarrollo y optimizacin, metodologas de desarrollo, o patrones de diseo, entre otros, conforman un
aspecto esencial a la hora de desarrollar un videojuego. Al igual que ocurre en otros
aspectos relacionados con la Ingeniera del Software, desde un punto de vista general
resulta aconsejable el uso de todos estos elementos para agilizar el proceso de desarrollo y reducir errores potenciales. En otras palabras, no es necesario, ni productivo,
reinventar la rueda cada vez que se afronta un nuevo proyecto.
1.1.2.
Tiempo real
En el mbito del desarrollo de videojuegos, el concepto de tiempo
real es muy importante para dotar
de realismo a los juegos, pero no
es tan estricto como el concepto de
tiempo real manejado en los sistemas crticos.
El desarrollo de videojuegos comerciales es un proceso complejo debido a los distintos requisitos que ha de satisfacer y a la integracin de distintas disciplinas que
intervienen en dicho proceso. Desde un punto de vista general, un videojuego es una
aplicacin grca en tiempo real en la que existe una interaccin explcita mediante
el usuario y el propio videojuego. En este contexto, el concepto de tiempo real se reere a la necesidad de generar una determinada tasa de frames o imgenes por segundo,
tpicamente 30 60, para que el usuario tenga una sensacin continua de realidad.
Por otra parte, la interaccin se reere a la forma de comunicacin existente entre el
usuario y el videojuego. Normalmente, esta interaccin se realiza mediante joysticks o
mandos, pero tambin es posible llevarla a cabo con otros dispositivos como por ejemplo teclados, ratones, cascos o incluso mediante el propio cuerpo a travs de tcnicas
de visin por computador o de interaccin tctil.
A continuacin se describe la estructura tpica de un equipo de desarrollo atendiendo a los distintos roles que juegan los componentes de dicho equipo [42]. En
muchos casos, y en funcin del nmero de componentes del equipo, hay personas
especializadas en diversas disciplinas de manera simultnea.
Los ingenieros son los responsables de disear e implementar el software que
permite la ejecucin del juego, as como las herramientas que dan soporte a dicha
ejecucin. Normalmente, los ingenieros se suelen clasicar en dos grandes grupos:
Los programadores del ncleo del juego, es decir, las personas responsables
de desarrollar tanto el motor de juego como el juego propiamente dicho.
Los programadores de herramientas, es decir, las personas responsables de
desarrollar las herramientas que permiten que el resto del equipo de desarrollo
pueda trabajar de manera eciente.
De manera independiente a los dos grupos mencionados, los ingenieros se pueden
especializar en una o en varias disciplinas. Por ejemplo, resulta bastante comn encontrar perles de ingenieros especializados en programacin grca o en scripting e IA.
Sin embargo, tal y como se sugiri anteriormente, el concepto de ingeniero transversal es bastante comn, particularmente en equipos de desarrollo que tienen un nmero
reducido de componentes o con un presupuesto que no les permite la contratacin de
personas especializadas en una nica disciplina.
1 http://www.idsoftware.com/games/quake/quake/
2 http://www.unrealengine.com/
3 http://www.havok.com
[6]
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
En el mundo del desarrollo de videojuegos, es bastante probable encontrar ingenieros senior responsables de supervisar el desarrollo desde un punto de vista tcnico,
de manera independiente al diseo y generacin de cdigo. No obstante, este tipo de
roles suelen estar asociados a la supervisin tcnica, la gestin del proyecto e incluso
a la toma de decisiones vinculadas a la direccin del proyecto. As mismo, algunas
compaas tambin pueden tener directores tcnicos, responsables de la supervisin
de uno o varios proyectos, e incluso un director ejecutivo, encargado de ser el director tcnico del estudio completo y de mantener, normalmente, un rol ejecutivo en la
compaa o empresa.
Los artistas son los responsables de la creacin de todo el contenido audio-visual
del videojuego, como por ejemplo los escenarios, los personajes, las animaciones de
dichos personajes, etc. Al igual que ocurre en el caso de los ingenieros, los artistas
tambin se pueden especializar en diversas cuestiones, destacando las siguientes:
Artistas de concepto, responsables de crear bocetos que permitan al resto del
equipo hacerse una idea inicial del aspecto nal 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.
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.
General VS Especco
En funcin del tamao de una empresa de desarrollo de videojuegos,
el nivel de especializacin de sus
empleados es mayor o menor. Sin
embargo, las ofertas de trabajo suelen incluir diversas disciplinas de
trabajo para facilitar su integracin.
C1
1.1. El desarrollo de videojuegos
[7]
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.
Scripting e IA
El uso de lenguajes de alto nivel
es bastante comn en el desarrollo
de videojuegos y permite diferenciar claramente la lgica de la aplicacin y la propia implementacin.
Una parte signicativa de las desarrolladoras utiliza su propio lenguaje de scripting, aunque existen lenguajes ampliamente utilizados, como son Lua o Python.
Los diseadores de juego son los responsables de disear el contenido del juego,
destacando la evolucin del mismo desde el principio hasta el nal, la secuencia de
captulos, las reglas del juego, los objetivos principales y secundarios, etc. Evidentemente, todos los aspectos de diseo estn estrechamente ligados al propio gnero del
mismo. Por ejemplo, en un juego de conduccin es tarea de los diseadores denir 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
planicacin 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 gura 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 preeren mantener una relacin temporal y asociarse con el
publicador que le ofrezca mejores condiciones para gestionar el lanzamiento de un
ttulo.
1.1.3.
El concepto de juego
Dentro del mundo del entretenimiento electrnico, un juego normalmente se suele asociar a la evolucin, entendida desde un punto de vista general, de uno o varios
personajes principales o entidades que pretenden alcanzar una serie de objetivos en
un mundo acotado, los cuales estn controlados por el propio usuario. As, entre estos
4 http://www.lua.org
5 http://www.python.org
[8]
CAPTULO 1. INTRODUCCIN
elementos podemos encontrar desde superhroes hasta coches de competicin pasando por equipos completos de ftbol. El mundo en el que conviven dichos personajes
suele estar compuesto, normalmente, por una serie de escenarios virtuales recreados
en tres dimensiones y tiene asociado una serie de reglas que determinan la interaccin
con el mismo.
De este modo, existe una interaccin explcita entre el jugador o usuario de videojuegos y el propio videojuego, el cual plantea una serie de retos al usuario con el
objetivo nal de garantizar la diversin y el entretenimiento. Adems de ofrecer este
componente emocional, los videojuegos tambin suelen tener un componente cognitivo asociado, obligando a los jugadores a aprender tcnicas y a dominar el comportamiento del personaje que manejan para resolver los retos o puzzles que los videojuegos
plantean.
Desde una perspectiva ms formal, la mayora de videojuegos suponen un ejemplo representativo de lo que se dene como aplicaciones grcas o renderizado en
tiempo real [6], las cuales se denen a su vez como la rama ms interactiva de la Informtica Grca. Desde un punto de vista abstracto, una aplicacin grca en tiempo
real se basa en un bucle donde en cada iteracin se realizan los siguientes pasos:
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 grca 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 sucientemente 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 dene como el nmero de
imgenes por segundo, comnmente fps, que la aplicacin grca 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.
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.
Aunque el componente grco 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
Cada de frames
Si el ncleo de ejecucin de un juego no es capaz de mantener los fps
a un nivel constante, el juego sufrir
una cada de frames en un momento determinado. Este hecho se denomina comnmente como ralentizacin.
C1
1.1. El desarrollo de videojuegos
[9]
real con un nivel de exactitud elevado no resulta manejable ni prctico, por lo que normalmente dicho mundo se aproxima y se simplica, 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 denir como agentes (agents) y se encuadran
dentro de la denominada simulacin basada en agentes [64]. 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, considerando la posibilidad de decisiones impredecibles a priori y las consecuencias que
podran desencadenar.
1.1.4.
Motor de juego
[10]
CAPTULO 1. INTRODUCCIN
Obviamente, la separacin entre motor de juego y juego nunca es total y, por una
circunstancia u otra, siempre existen dependencias directas que no permiten la reusabilidad completa del motor para crear otro juego. La dependencia ms evidente es el
genero al que est vinculado el motor de juego. Por ejemplo, un motor de juegos diseado para construir juegos de accin en primera persona, conocidos tradicionalmente
como shooters o shootem all, ser difcilmente reutilizable para desarrollar un juego
de conduccin.
Una forma posible para diferenciar un motor de juego y el software que representa
a un juego est asociada al concepto de arquitectura dirigida por datos (data-driven
architecture). Bsicamente, cuando un juego contiene parte de su lgica o funcionamiento en el propio cdigo (hard-coded logic), entonces no resulta prctico reutilizarla para otro juego, ya que implicara modicar el cdigo fuente sustancialmente. Sin
embargo, si dicha lgica o comportamiento no est denido a nivel de cdigo, sino
por ejemplo mediante una serie de reglas denidas a travs de un lenguaje de script,
entonces la reutilizacin s es posible y, por lo tanto, beneciosa, ya que optimiza el
tiempo de desarrollo.
Como conclusin nal, resulta relevante destacar la evolucin relativa a la generalidad de los motores de juego, ya que poco a poco estn haciendo posible su utilizacin
para diversos tipos de juegos. Sin embargo, el compromiso entre generalidad y optimalidad an est presente. En otras palabras, a la hora de desarrollar un juego utilizando
un determinado motor es bastante comn personalizar dicho motor para adaptarlo a
las necesidades concretas del juego a desarrollar.
1.1.5.
Gneros de juegos
Los motores de juegos suelen estar, generalmente, ligados a un tipo o gnero particular de juegos. Por ejemplo, un motor de juegos diseado con la idea de desarrollar
juegos de conduccin diferir en gran parte con respecto a un motor orientado a juegos
de accin en tercera persona. No obstante, y tal y como se discutir en la seccin 1.2,
existen ciertos mdulos, sobre todo relativos al procesamiento de ms bajo nivel, que
son transversales a cualquier tipo de juego, es decir, que se pueden reutilizar en gran
medida de manera independiente al gnero al que pertenezca el motor. Un ejemplo
representativo podra ser el mdulo de tratamiento de eventos de usuario, es decir, el
mdulo responsable de recoger y gestionar la interaccin del usuario a travs de dispositivos como el teclado, el ratn, el joystick o la pantalla tctil. Otros ejemplo podra
ser el mdulo de tratamiento del audio o el mdulo de renderizado de texto.
A continuacin, se realizar una descripcin de los distintos gneros de juegos
ms populares atendiendo a las caractersticas que diferencian unos de otros en base
al motor que les da soporte. Esta descripcin resulta til para que el desarrollador identique los aspectos crticos de cada juego y utilice las tcnicas de desarrollo adecuadas
para obtener un buen resultado.
Probablemente, el gnero de juegos ms popular ha sido y es el de los los denominados FPS, abreviado tradicionalmente como shooters, representado por juegos como
Quake, Half-Life, Call of Duty o Gears of War, entre muchos otros. En este gnero,
el usuario normalmente controla a un personaje con una vista en primera persona a lo
largo de escenarios que tradicionalmente han sido interiores, como los tpicos pasillos,
pero que han ido evolucionando a escenarios exteriores de gran complejidad.
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 [42]:
Mercado de shooters
Los FPS (First Person Shooter) gozan actualmente de un buen momento y, como consecuencia de
ello, el nmero de ttulos disponibles es muy elevado, ofreciando una
gran variedad al usuario nal.
C1
1.1. El desarrollo de videojuegos
[11]
Figura 1.5: Captura de pantalla del juego Tremulous R , licenciado bajo GPL y desarrollado sobre el motor
de Quake III.
[12]
CAPTULO 1. INTRODUCCIN
7 http://www.idsoftware.com/business/techdownloads
8 http://mycryengine.com/
El popular juego de Mario, diseado en 1985 por Shigeru Miyamoto, ha vendido aproximadamente 40
millones de juegos a nivel mundial.
Segn el libro de los Record Guinness, es una de los juegos ms vendidos junto a Tetris y a la saga de
Pokemon.
C1
1.1. El desarrollo de videojuegos
[13]
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 [42]:
Grcos 3D
Virtua Fighter, lanzado en 1993 por
Sega y desarrollado por Yu Suzuki,
se considera como el primer juego
de lucha arcade en soportar grcos
tridimensionales.
Simuladores F1
Los simuladores de juegos de conduccin no slo se utilizan para
el entretenimiento domstico sino
tambin para que, por ejemplo, los
pilotos de Frmula-1 conozcan todos los entresijos de los circuitos y
puedan conocerlos al detalle antes
de embarcarse en los entrenamientos reales.
Los juegos del gnero de la lucha han de prestar atencin a la deteccin y gestin
de colisiones entre los propios luchadores, o entre las armas que utilicen, para dar una
sensacin de mayor realismo. Adems, el mdulo responsable del tratamiento de la
entrada al usuario ha de ser lo sucientemente sosticado para gestionar de manera
adecuada las distintas combinaciones de botones necesarias para realizar complejos
movimientos. Por ejemplo, juegos como Street Fighter IV incorporan un sistema de
timing entre los distintos movimientos de un combo. El objetivo perseguido consiste
en que dominar completamente a un personaje no sea una tarea sencilla y requiera que
el usuario de videojuegos dedique tiempo al entrenaiento del mismo.
Los juegos de lucha, en general, han estado ligados a la evolucin de tcnicas
complejas de sntesis de imagen aplicadas sobre los propios personajes con el objetivo
de mejorar al mximo su calidad y, de este modo, incrementar su realismo. Un ejemplo
representativo es el uso de shaders [76] sobre la armadura o la propia piel de los
personajes que permitan implementar tcnicas como el bump mapping [6], planteada
para dotar a estos elementos de un aspecto ms rugoso.
[14]
CAPTULO 1. INTRODUCCIN
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 clasicados 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 ja, 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 edicios, sino tambin la planicacin 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 nmero de unidades de manera
simultnea.
Finalmente, en los ltimos aos ha aparecido un gnero 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.
C1
1.2. Arquitectura del motor. Visin general
[15]
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 denir 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.
1.2.1.
La arquitectura Cell
En arquitecturas ms novedosas,
como por ejemplo la arquitectura
Cell usada en Playstation 3 y desarrollada por Sony, Toshiba e IBM,
las optimizaciones aplicadas suelen
ser ms dependientes de la plataforma nal.
[16]
CAPTULO 1. INTRODUCCIN
Subsistema
de juego
Networking
Motor de
rendering
Herramientas
de desarrollo
Audio
Motor
de fsica
Interfaces
de usuario
Gestor de recursos
Subsistemas principales
Capa independiente de la plataforma
Software development kits (SDKs) y middlewares
Sistema operativo
Drivers
Hardware
Figura 1.9: Visin conceptual de la arquitectura general de un motor de juegos. Esquema adaptado de la
arquitectura propuesta en [42].
1.2.2.
SDKs
y middlewares
APIs grcas
OpenGL y Direct3D son los dos
ejemplos ms representativos de
API (Application Program Interface)s grcas que se utilizan en el
mbito comercial. La principal diferencia entre ambas es la estandarizacin, factor que tiene sus ventajas y desventajas.
C1
1.2. Arquitectura del motor. Visin general
[17]
1.2.3.
Abstraccin funcional
Aunque en teora las herramientas multiplataforma deberan abstraer de los aspectos subyacentes
a las mismas, como por ejemplo
el sistema operativo, en la prctica
suele ser necesario realizar algunos
ajustos en funcin de la plataforma
existente en capas de nivel inferior.
Gran parte de los juegos se desarrollan teniendo en cuenta su potencial lanzamiento en diversas plataformas. Por ejemplo, un ttulo se puede desarrollar para diversas
consolas de sobremesa y para PC al mismo tiempo. En este contexto, es bastante comn encontrar una capa software que aisle al resto de capas superiores de cualquier
aspecto que sea dependiente de la plataforma. Dicha capa se suele denominar capa
independiente de la plataforma.
Aunque sera bastante lgico suponer que la capa inmediatamente inferior, es decir, la capa de SDKs y middleware, ya posibilita la independencia respecto a las plataformas subyacentes debido al uso de mdulos estandarizados, como por ejemplo
bibliotecas asociadas a C/C++, la realidad es que existen diferencias incluso en bibliotecas estandarizadas para distintas plataformas.
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 grca.
10 http://www.sgi.com/tech/stl/
11 http://http://www.opengl.org/
12 http://www.havok.com
13 http://www.ode.org
[18]
CAPTULO 1. INTRODUCCIN
1.2.4.
Subsistemas principales
1.2.5.
Gestor de recursos
Ogre 3D
El motor de rendering Ogre 3D est escrito en C++ y permite que
el desarrollador se abstraiga de un
gran nmero de aspectos relativos
al desarrollo de aplicaciones grcas. Sin embargo, es necesario estudiar su funcionamiento y cmo utilizarlo de manera adecuada.
Shaders
Un shader se puede denir como
un conjunto de instrucciones software que permiten aplicar efectos
de renderizado a primitivas geomtricas. Al ejecutarse en las unidades
de procesamiento grco (Graphic
Processing Units - GPUs), el rendimiento de la aplicacin grca mejora considerablemente.
C1
1.2. Arquitectura del motor. Visin general
[19]
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 [42].
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.
1.2.6.
Motor de rendering
[20]
CAPTULO 1. INTRODUCCIN
Front end
Efectos visuales
Motor de rendering
Figura 1.12: Visin conceptual de la arquitectura general de un motor de rendering. Esquema simplicado
de la arquitectura discutida en [42].
La capa de renderizado de bajo nivel aglutina las distintas utilidades de renderizado del motor, es decir, la funcionalidad asociada a la representacin grca de las
distintas entidades que participan en un determinado entorno, como por ejemplo cmaras, primitivas de rendering, materiales, texturas, etc. El objetivo principal de esta
capa reside precisamente en renderizar las distintas primitivas geomtricas tan rpido como sea posible, sin tener en cuenta posibles optimizaciones ni considerar, por
ejemplo, qu partes de las escenas son visibles desde el punto de vista de la cmara.
Esta capa tambin es responsable de gestionar la interaccin con las APIs de programacin grcas, como OpenGL o Direct3D, simplemente para poder acceder a
los distintos dispositivos grcos que estn disponibles. Tpicamente, este mdulo se
denomina interfaz de dispositivo grco (graphics device interface).
As mismo, en la capa de renderizado de bajo nivel existen otros componentes
encargados de procesar el dibujado de distintas primitivas geomtricas, as como de la
gestin de la cmara y los diferentes modos de proyeccin. En otras palabras, esta capa
proporciona una serie de abstracciones para manejar tanto las primitivas geomtricas
como las cmaras virtuales y las propiedades vinculadas a las mismas.
Optimizacin
Las optimizaciones son esenciales
en el desarrollo de aplicaciones grcas, en general, y de videojuegos,
en particular, para mejorar el rendimiento. Los desarrolladores suelen hacer uso de estructuras de datos auxiliares para aprovecharse del
mayor conocimiento disponible sobre la propia aplicacin.
C1
1.2. Arquitectura del motor. Visin general
[21]
Por otra parte, dicha capa tambin gestiona el estado del hardware grco y los
shaders asociados. Bsicamente, cada primitiva recibida por esta capa tiene asociado
un material y se ve afectada por diversas fuentes de luz. As mismo, el material describe la textura o texturas utilizadas por la primitiva y otras cuestiones como por ejemplo
qu pixel y vertex shaders se utilizarn para renderizarla.
La capa superior a la de renderizado de bajo nivel se denomina scene graph/culling y optimizaciones y, desde un punto de vista general, es la responsable de
seleccionar qu parte o partes de la escena se enviarn a la capa de rendering. Esta seleccin, u optimizacin, permite incrementar el rendimiento del motor de rendering,
debido a que se limita el nmero de primitivas geomtricas enviadas a la capa de nivel
inferior.
Aunque en la capa de rendering slo se dibujan las primitivas que estn dentro del
campo de visin de la cmara, es decir, dentro del viewport, es posible aplicar ms
optimizaciones que simpliquen la complejidad de la escena a renderizar, obviando
aquellas partes de la misma que no son visibles desde la cmara. Este tipo de optimizaciones son crticas en juegos que tenga una complejidad signicativa con el objetivo
de obtener tasas de frames por segundo aceptables.
Una de las optimizaciones tpicas consiste en hacer uso de estructuras de datos de
subdivisin espacial para hacer ms eciente el renderizado, gracias a que es posible
determinar de una manera rpida el conjunto de objetos potencialmente visibles. Dichas estructuras de datos suelen ser rboles, aunque tambin es posible utilizar otras
alternativas. Tradicionalmente, las subdivisiones espaciales se conocen como scene
graph (grafo de escena), aunque en realidad representan un caso particular de estructura de datos.
Por otra parte, en esta capa tambin es comn integrar mtodos de culling, como
por ejemplo aquellos basados en utilizar informacin relevante de las oclusiones para
determinar qu objetos estn siendo solapados por otros, evitando que los primeros se
tengan que enviar a la capa de rendering y optimizando as este proceso.
Idealmente, esta capa debera ser independiente de la capa de renderizado, permitiendo as aplicar distintas optimizaciones y abstrayndose de la funcionalidad relativa al dibujado de primitivas. Un ejemplo representativo de esta independencia est
representado por OGRE (Object-Oriented Graphics Rendering Engine) y el uso de la
losofa plug & play, de manera que el desarrollador puede elegir distintos diseos de
grafos de escenas ya implementados y utilizarlos en su desarrollo.
Filosofa Plug & Play
Esta losofa se basa en hacer uso
de un componente funcional, hardware o software, sin necesidad de
congurar ni de modicar el funcionamiento de otros componentes
asociados al primero.
[22]
CAPTULO 1. INTRODUCCIN
1.2.7.
Herramientas de depuracin
Versiones beta
Adems del uso extensivo de herramientas de depuracin, las desarrolladoras de videojuegos suelen liberar versiones betas de los mismos
para que los propios usuarios contribuyan en la deteccin de bugs.
1.2.8.
Motor de fsica
ED auxiliares
Al igual que ocurre en procesos como la obtencin de la posicin de
un enemigo en el mapa, el uso extensivo de estructuras de datos auxiliares permite obtener soluciones
a problemas computacionalmente
complejos. La gestin de colisiones
es otro proceso que se benecia de
este tipo de tcnicas.
C1
1.2. Arquitectura del motor. Visin general
[23]
Por otra parte, algunos juegos incluyen sistemas realistas o semi-realistas de simulacin dinmica. En el mbito de la industria del videojuego, estos sistemas se suelen
denominar sistema de fsica y estn directamente ligados al sistema de gestin de
colisiones.
Actualmente, la mayora de compaas utilizan motores de colisin/fsica desarrollados por terceras partes, integrando estos kits de desarrollo en el propio motor. Los
ms conocidos en el mbito comercial son Havok, el cual representa el estndar de
facto en la industria debido a su potencia y rendimiento, y PhysX, desarrollado por
NVIDIA e integrado en motores como por ejemplo el Unreal Engine 3.
En el mbito del open source, uno de los ms utilizados es ODE. Sin embargo,
en este curso se har uso del motor de simulacin fsica Bullet14 , el cual se utiliza
actualmente en proyectos tan ambiciosos como la suite 3D Blender.
1.2.9.
Interfaces de usuario
En cualquier tipo de juego es necesario desarrollar un mdulo que ofrezca una abstraccin respecto a la interaccin del usuario, es decir, un mdulo que principalmente
sea responsable de procesar los eventos de entrada del usuario. Tpicamente, dichos
eventos estarn asociados a la pulsacin de una tecla, al movimiento del ratn o al uso
de un joystick, entre otros.
Desde un punto de vista ms general, el mdulo de interfaces de usuario tambin
es responsable del tratamiento de los eventos de salida, es decir, aquellos eventos
que proporcionan una retroalimentacin al usuario. Dicha interaccin puede estar representada, por ejemplo, por el sistema de vibracin del mando de una consola o por
la fuerza ejercida por un volante que est siendo utilizado en un juego de conduccin.
Debido a que este mdulo gestiona los eventos de entrada y de salida, se suele denominar comnmente componente de entrada/salida del jugador (player I/O component).
El mdulo de interfaces de usuario acta como un puente entre los detalles de bajo
nivel del hardware utilizado para interactuar con el juego y el resto de controles de
ms alto nivel. Este mdulo tambin es responsable de otras tareas importantes, como
la asocacin de acciones o funciones lgicas al sistema de control del juego, es decir,
permite asociar eventos de entrada a acciones lgicas de alto nivel.
En la gestin de eventos se suelen utilizar patrones de diseo como el patrn delegate [37], de manera que cuando se detecta un evento, ste se traslada a la entidad
adecuada para llevar a cabo su tratamiento.
1.2.10.
Lag
El retraso que se produce desde que
se enva un paquete de datos por
una entidad hasta que otra lo recibe
se conoce como lag. En el mbito
de los videojuegos, el lag se suele
medir en milsimas de segundo.
Networking y multijugador
[24]
CAPTULO 1. INTRODUCCIN
1.2.11.
Subsistema de juego
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 simplicado
de la arquitectura discutida en [42].
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 denir, por ejemplo, el comportamiento de los personajes que participan en el
juego.
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.
C1
1.2. Arquitectura del motor. Visin general
[25]
Im all ears!
El apartado sonoro de un juego es
especialmente importante para que
el usuario se sienta inmerso en el
mismo y es crtico para acompaar
de manera adecuada el desarrollo de
dicho juego.
[26]
1.2.12.
CAPTULO 1. INTRODUCCIN
Audio
1.2.13.
Captulo
Herramientas de Desarrollo
Cleto Martn Angelina
ctualmente, existen un gran nmero de aplicaciones y herramientas que permiten a los desarrolladores de videojuegos, y de aplicaciones en general, aumentar su productividad a la hora de construir software, gestionar los proyectos y
recursos, as como automatizar procesos de construccin.
En este captulo, se pone de maniesto la importancia de la gestin en un proyecto software y se muestran algunas de las herramientas de desarrollo ms conocidas
en sistemas GNU/Linux. La eleccin de este tipo de sistema no es casual. Por un lado, se trata de Software Libre, lo que permite a desarrolladores estudiar, aprender y
entender lo que hace el cdigo que se ejecuta. Por otro lado, probablemente sea el
mejor sistema operativo para construir y desarrollar aplicaciones debido al gran nmero de herramientas que proporciona. Es un sistema hecho por programadores para
programadores.
2.1.
Introduccin
En la construccin de software no trivial, las herramientas de gestin de proyectos y de desarrollo facilitan la labor de las personas que lo construyen. Conforme el
software se va haciendo ms complejo y se espera ms funcionalidad de l, se hace
necesario el uso de herramientas que permitan automatizar los procesos del desarrollo,
as como la gestin del proyecto y su documentacin.
Adems, dependiendo del contexto, es posible que existan otros integrantes del
proyecto que no tengan formacin tcnica y que necesiten realizar labores sobre el
producto como traducciones, pruebas, diseo grco, etc.
27
[28]
Los videojuegos son proyectos software que, normalmente, requieren la participacin de varias personas con diferentes perles profesionales (programadores, diseadores grcos, compositores de sonidos, etc.). Cada uno de ellos, trabaja con diferentes tipos de datos en diferentes tipos de formatos. Todas las herramientas que permitan
la construccin automtica del proyecto, la integracin de sus diferentes componentes
y la coordinacin de sus miembros sern de gran ayuda en un entorno tan heterogneo.
Desde el punto de vista de la gestin del proyecto, una tarea esencial es la automatizacin del proceso de compilacin y de construccin de los programas. Una de las
tareas que ms frecuentemente se realizan mientras se desarrolla y depura un programa
es la de compilacin y construccin. Cuanto ms grande y complejo sea un programa,
mayor es el tiempo que se pierde en esta fase. Por tanto, un proceso automtico de
construccin de software ahorrar muchas prdidas de tiempo en el futuro.
En los sistemas GNU/Linux es habitual el uso de herramientas como el compilador
GCC, el sistema de construccin GNU Make y el depurador GDB. Todas ellas creadas en el proyecto GNU y orientadas a la creacin de programas en C/C++, aunque
tambin pueden ser utilizadas con otras tecnologas. Tambin existen editores de texto
como GNU Emacs o vi, y modernos (pero no por ello mejores) entornos de desarrollo
como Eclipse que no slo facilitan las labores de escritura de cdigo, sino que proporcionan numerosas herramientas auxiliares dependiendo del tipo de proyecto. Por
ejemplo, Eclipse puede generar los archivos Makeles necesarios para automatizar el
proceso de construccin con GNU Make.
2.2.
Figura 2.1: El proyecto GNU proporciona una gran abanico de herramientas de desarrollo y son utilizados en proyectos software de todo
tipo.
2.2.1.
Conceptos bsicos
A la hora de buscar y compartir informacin con el resto de compaeros de profesin es necesario el uso de una terminologa comn. En las tareas de construccin del
software existen algunos trminos que son importantes conocer.
[29]
C2
[30]
[31]
2.2.2.
Desde un punto de vista estricto, GCC no es un compilador. GNU Compiler Collection (GCC) es un conjunto de compiladores que proporciona el proyecto GNU para
diferentes lenguajes de programacin tales como C, C++, Java, FORTRAN, etc. Dentro de este conjunto de compiladores, G++ es el compilador para C++. Teniendo en
cuenta esta precisin, es comn llamar simplemente GCC al compilador de C y C++,
por lo que se usarn de forma indistinta en este texto.
En esta seccin se introducen la estructura bsica del compilador GCC, as como
algunos de sus componentes y su papel dentro del proceso de compilacin. Finalmente, se muestran ejemplos de cmo utilizar GCC (y otras herramientas auxiliares) para
construir un ejecutable, una biblioteca esttica y una dinmica.
2.2.3.
GCC es un compilador cuya estructura es muy similar a la presentada en la seccin 2.2.1. Sin embargo, cada una de las fases de compilacin la realiza un componente bien denido e independiente. Concretamente, al principio de la fase de compilacin, se realiza un procesamiento inicial del cdigo fuente utilizando el preprocesador
GNU CPP, posteriormente se utiliza GNU Assembler para obtener el cdigo objeto y,
con la ayuda del enlazador GNU ld, se crea el binario nal.
distcc
La compilacin modular y por fases permite que herramientas como
distcc puedan realizar compilaciones distribuidas en red y en paralelo.
El hecho de que est dividido en estas etapas permite una compilacin modular,
es decir, cada chero de entrada se transforma a cdigo objeto y con la ayuda del enlazador se resuelven las dependencias que puedan existir entre ellos. A continuacin,
se comenta brevemente cada uno de los componentes principales.
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:
C2
[32]
#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 congurados para
obtener diferentes mensajes de advertencia (warnings) a diferentes niveles.
[33]
Comprobacin y resolucin de smbolos y dependencias a nivel de declaracin.
Realizar optimizaciones.
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
GNU Assembler forma parte de la
distribucin GNU Binutils y se corresponde con el programa as.
GNU Linker
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 nal. Algunas de las tareas que se realizan en el proceso de
enlazado son las siguientes:
Seleccin y ltrado de los objetos necesarios para la generacin del binario.
Comprobacin y resolucin de smbolos y dependencias a nivel de denicin.
Realizacin del enlazado (esttico y dinmico) de las bibliotecas.
Como ejercicio se propone utilizar el linker directamente con el cdigo objeto
generado en el apartado anterior. Ntese que las opciones -l y -L sirven para aadir
rutas personalizadas a las que por defecto ld utiliza para buscar bibliotecas.
2.2.4.
Ejemplos
C2
[34]
Compilacin de un ejecutable
Como ejemplo de ejecutable se toma el siguiente programa.
#include <iostream>
using namespace std;
class Square {
private:
int side_;
public:
Square(int side_length) : side_(side_length) { };
int getArea() const { return side_*side_; };
};
int main () {
Square square(5);
cout << "Area: " << square.getArea() << endl;
return 0;
}
En C++, los programas que generan un ejecutable deben tener denida la funcin main, que ser el punto de entrada de la ejecucin. El programa es trivial: se
dene 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:
$ g++ -o main main.cpp
[35]
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_;
}
#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
C2
[36]
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>
class Square : public Figure {
private:
float side_;
public:
Square(float side_length);
float getArea() const;
};
[37]
#include <Figure.h>
class Triangle : public Figure {
private:
float base_;
float height_;
public:
Triangle(float base_, float height_);
float getArea() const;
};
height_(height) { }
#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); }
C2
[38]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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;
}
La ruta por defecto en la que se busca las bibliotecas instaladas en el sistema depende de la distribucin GNU/Linux que se utilice. Normalmente, se
encuentran en /lib y /usr/lib.
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 predenadas donde se buscarn las bibliotecas. Por defecto, son
las mismas que para el proceso de enlazado.
Tambin es posible aadir rutas modicando la variable LD_LIBRARY_PATH:
$ LD_LIBRARY_PATH=. ./main
[39]
2.2.5.
Otras herramientas
2.2.6.
Los programas tienen fallos y los programadores cometen errores. Los compiladores ayudan a la hora de detectar errores lxicos, sintcticos y semnticos del lenguaje
de entrada. Sin embargo, el compilador no puede deducir (por lo menos hasta hoy) la
lgica que encierra el programa, su signicado nal o su propsito. Estos errores se
conocen como errores lgicos.
Los depuradores son programas que facilitan la labor de deteccin de errores, sobre todo los lgicos. Con un depurador, el programador puede probar una ejecucin
paso por paso, examinar/modicar el contenido de las variables en un cierto momento, etc. En general, se pueden realizar las tareas necesarias para conseguir reproducir
y localizar un error difcil de detectar a simple vista. Muchos entornos de desarrollo
como Eclipse, .NET o Java Beans incorporan un depurador para los lenguajes soportados. Sin duda alguna, se trata de una herramienta esencial en cualquier proceso de
desarrollo software.
En esta seccin se muestra el uso bsico de GNU Debugger (GDB) , un depurador
libre para sistemas GNU/Linux que soporta diferentes lenguajes de programacin,
entre ellos C++.
1 Ms
informacin en http://www.gnu.org/software/binutils/
C2
[40]
Para depurar no se debe hacer uso de las optimizaciones. stas pueden generar cdigo que nada tenga que ver con el original.
#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;
}
[41]
$ ./main
Main start
Function A: 24
Segmentation fault
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 para el programa, 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)
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
especicar sus acciones.
Examinando el contenido
Abreviatura
Todas las rdenes de GDB pueden
escribirse utilizando su abreviatura.
Ej: run = r.
C2
[42]
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
mantener el nivel de la ejecucin (stack frame), es decir, entra en la denicin
de la funcin (si la hubiere).
stepi se comporta igual que step pero a nivel de instrucciones mquina.
next (n): ejecuta la instruccin actual y salta a la siguiente manteniendo el
stack frame, es decir, la denicin de la funcin se toma como una instruccin
atmica.
nexti se comporta igual que next pero si la instruccin es una llamada a
funcin se espera a que termine.
A continuacin se va a utilizar step para avanzar en la ejecucin del programa.
Ntese que para repetir la ejecucin de step basta con introducir una orden vaca.
En este caso, GDB vuelve a ejecutar la ltima orden.
(gdb) s
Main start
27
cout << "Return A: " << functionA(24) << endl;
(gdb)
functionA (a=24) at main.cpp:18
18
cout << "Function A: " << a << endl;
(gdb)
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)
[43]
Breakpoints
La ejecucin paso a paso es una herramienta til para una depuracin de grano
no. 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 ujo del programa en un punto determinado por el usuario.
Con el ejemplo ya arreglado, se congura 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 n 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;
C2
[44]
(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:
Watchpoints: la ejecucin se detiene cuando una determinada expresin cambia.
Catchpoints: la ejecucin se detiene cuando se produce un evento, como una
excepcin o la carga de una librera dinmica.
Stack y frames
En muchas ocasiones, los errores vienen debidos a que las llamadas a funciones no
se realizan con los parmetros adecuados. Es comn pasar punteros no inicializados o
valores incorrectos a una funcin/mtodo y, por tanto, obtener un error lgico.
Para gestionar las llamadas a funciones y procedimientos, en C/C++ se utiliza la
pila (stack ). En la pila se almacenan frames, estructuras de datos que registran las
variables creadas dentro de una funcin as como otra informacin de contexto. GDB
permite manipular la pila y los frames de forma que sea posible identicar un uso
indebido de las funciones.
Con la ejecucin parada en functionB(), se puede mostrar el contenido de la
pila con la orden backtrace (bt):
(gdb) backtrace
#0 functionB (str1=..., t=0x602010) at main.cpp:13
#1 0x0000000000400d07 in functionA (a=24) at main.cpp:21
#2 0x0000000000400db3 in main () at main.cpp:27
(gdb)
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 modicar las variables y argumentos:
(gdb) print *t
$1 = {_value = 15}
(gdb) call t->setValue(1000)
(gdb) print *t
$2 = {_value = 1000}
(gdb)
Invocar funciones
La orden call se puede utilizar para invocar funciones y mtodos .
[45]
gdb-mode: modo de Emacs para GDB. Dentro del modo se puede activar la
opcin M-x many-windows para obtener buffers con toda la informacin
disponible.
kdbg: ms atractivo grcamente (para escritorios KDE).
2.2.7.
# Variable definitions
VAR1=/home/user
export VAR2=yes
# Rules
target1: dependency1 dependency2 ...
action1
action2
dependency1: dependency3
action3
action4
C2
[46]
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.
Tomando como ejemplo la aplicacin que hace uso de la biblioteca dinmica, el
siguiente listado muestra el Makele que generara tanto el programa ejecutable como
la biblioteca esttica:
Listado 2.17: Makele 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
[47]
$ make
$ make clean
C2
[48]
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 se denen ciertas variables de usuario, Make puede deducir cmo construir los objetivos.
A continuacin, se transforma el ejemplo anterior utilizando las reglas implcitas:
Listado 2.19: Makele con reglas implcitas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 chero fuente (que es lo habitual). Por
ello, no es necesario especicar 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 chero llamado main.cpp).
Las variables de usuario que se han denido permiten congurar los ags de compilacin que se van a utilizar en las reglas explcitas. As:
CC: la orden que se utilizar como compilador. En este caso, el de C++.
CXXFLAGS: los ags para el preprocesador y compilador de C++ (ver seccin 2.2.3). Aqu slo se dene la opcin -I, pero tambin es posible aadir
optimizaciones y la opcin de depuracin -ggdb o -fPIC para las bibliotecas
dinmicas.
LDFLAGS: ags para el enlazador (ver seccin 2.2.3). Aqu se denen las rutas
de bsqueda de las bibliotecas estticas y dinmicas.
LDLIBS: en esta variable se especican las opciones de enlazado. Normalmente, basta con utilizar la opcin -l con las bibliotecas necesarias para generar el
ejecutable.
[49]
Funciones
GNU Make proporciona un conjunto de funciones que pueden ser de gran ayuda
a la hora de construir los Makeles. Muchas de las funciones estn diseadas para
el tratamiento de cadenas, ya que se suelen utilizar para transformar los nombres de
archivos. Sin embargo, existen muchas otras como para realizar ejecucin condicional,
bucles y 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 Makele, desde las acciones de una regla hasta en la denicin de un variable. En el siguiente listado se
muestra el uso de algunas de estas funciones:
Listado 2.20: Makele con reglas implcitas
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)
C2
[50]
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 predenidos se recomiendo utilizar el manual en lnea de Make2 , el cual siempre se encuentra
actualizado.
2.3.
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 grco que permita
entender mejor los componentes del mismo y, por ello, asegure una mayor mantenibilidad para el futuro (manuales, diagramas, especicaciones, etc.).
La gestin del proyecto es un proceso transversal al resto de procesos y tareas y se
ocupa de la planicacin 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 noticar 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 especcas para un proceso concreto.
2.3.1.
[51]
Por otro lado, sera interesante tener la posibilidad de realizar desarrollos en paralelo de forma que exista una versin estable de todo el proyecto y otra ms experimental del mismo donde se probaran diferentes algoritmos y diseos. De esta forma,
probar el impacto que tendra nuevas implementaciones sobre el proyecto no afectara a una versin ms ocial. Tambin es comn que se desee aadir una nueva
funcionalidad y sta se realiza en paralelo junto con otros desarrollos.
Los sistemas de control de versiones o Version Control System (VCS) permiten
gestionar los archivos de un proyecto (y sus versiones) y que sus integrantes puedan
acceder remotamente a ellos para descargarlos, modicarlos y publicar los cambios.
Tambin se encargan de detectar posibles conictos cuando varios usuarios modican
los mismos archivos y de proporcionar un sistema bsico de registro de cambios.
Como norma general, al VCS debe subirse el archivo fuente y nunca el archivo
generado. No se deben subir binarios ya que no es fcil seguir la pista a sus
modicaciones.
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 modicaciones, 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
conictos entre las modicaciones 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 ja un nodo como
servidor, pero se perderan algunas posibilidades que este esquema ofrece.
C2
[52]
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
Inicialmente, los clientes pueden descargarse el repositorio por primera vez utilizando la orden checkout:
$ svn checkout svn+ssh://user1@myserver:/var/repo/myproject
Checked out revision X.
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
[53]
Ntese que este commit slo aade los cambios hechos en new_file, aceptando
los cambios en other_file que hizo user2.
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 modicada 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
Al igual que ocurre con Subversion, este directorio debe ser accesible mediante algn mecanismo (preferiblemente, que sea seguro) para que el resto de usuarios
pueda acceder. Sin embargo, el usuario user1 puede trabajar directamente sobre ese
directorio.
C2
[54]
Para obtener una versin inicial, otro usuario (user2) debe clonar el repositorio.
Basta con ejecutar lo siguiente:
$ 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 gura 2.9, 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 modicado.
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.
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 conictos con otros usuarios, una buena costumbre antes de realizar un push es conveniente obtener los posibles cambios en el servidor con
pull y update.
[55]
Para ver cmo se gestionan los conictos en Mercurial, supngase que user1
realiza lo siguiente:
$ echo "A file" > a_file
$ hg add a_file
$ hg commit
Al intentar realizar el push y entrar en conicto, Mercurial avisa de ello deteniendo la carga. En este punto se puede utilizar la opcin -f para forzar la operacin de
push, lo cual creara un nuevo head. Como resultado, se creara una nueva rama a
partir de ese conicto de forma que se podra seguir desarrollando omitiendo el conicto. Si en el futuro se pretende unir los dos heads se utilizan las operaciones merge
y resolve.
La otra solucin, y normalmente la ms comn, es obtener los cambios con pull,
unicar heads (merge), resolver los posibles conictos manualmente si es necesario
(resolve), hacer commit de la solucin dada (commit) y volver a intentar la subida
(push):
hgview
hgview es una herramienta grca
que permite visualizar la evolucin
de las ramas y heads de un proyecto
que usa Mercurial.
$ 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. En trminos generales, tiene una estructura similar a Mercurial: independencia
entre repositorio remotos y locales, gestin local de cambios, etc.
Sin embargo, Git es en ocasiones preferido sobre Mercurial por algunas de sus
caractersticas propias:
C2
[56]
[57]
example
Ntese como la ltima llamada a status no muestra ningn cambio por subir al
repositorio local. Esto signica 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:
C2
[58]
$ 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
Para modicar cdigo y subirlo al repositorio local se sigue el mismo procedimiento: (1) realizar la modicacin, (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. 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, modica un archivo en esa rama y nalmente 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 identicadores 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
gitk
Para entender mejor estos conceptos y visualizarlos durante el proceso de desarrollo, existen herramientas grcas como gitk que permiten ver todos los commits y heads
en cada instante.
[59]
Git utiliza cheros como .gitconfig y .gitignore para cargar conguraciones personalizadas e ignorar cheros a la hora de hacer los commits,
respectivamente. Son muy tiles. Revisa la documentacin de git-config
y gitignore para ms informacin.
2.3.2.
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 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.
C2
[60]
8
/**
9
\param s the name of the Test.
10
*/
11
Test(string s);
12
13
/// Start running the test.
14
/**
15
\param max maximum time of test delay.
16
\param silent if true, do not provide output.
17
\sa Test()
18
*/
19
int run(int max, bool silent);
20 };
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 modicar 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
The main title
3 ================
4
5 This is an example of document in ReStructured Text (RST). You can
get
docutils.sourceforge.net/docs/ref/rst/restructuredtext.html>_.
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 |
+--------------+----------+-----------+-----------+
30
31
32
33
34
35
36
37
38
[61]
| 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.
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, cargar los mismos en
el programa, representar conguraciones, 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 ocial, 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
--- !<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:
4 http://www.yaml.org
C2
[62]
27
28
29
2.3.3.
Forjas de desarrollo
Hasta ahora, se han mostrado herramientas especcas que permiten crear y gestionar los elementos ms importantes de un proyecto software: los archivos que lo
forman y su documentacin. Sin embargo, existen otras como la gestin de tareas,
los mecanismos de noticacin de errores o los sistemas de comunicacin con los
usuarios que son de utilidad en un proyecto.
Las forjas de desarrollo son sistemas colaborativos que integran no slo herramientas bsicas para la gestin de proyectos, como un VCS, sino que suelen proporcionar
herramientas para:
Planicacin y gestin de tareas: permite anotar qu tareas quedan por hacer
y los plazos de entrega. Tambin suelen permitir asignar prioridades.
Planicacin y gestin de recursos: ayuda a controlar el grado de ocupacin
del personal de desarrollo (y otros recursos).
Seguimiento de fallos: tambin conocido como bug tracker, es esencial para
llevar un control sobre los errores encontrados en el programa. Normalmente,
permiten gestionar el ciclo de vida de un fallo, desde que se descubre hasta que
se da por solucionado.
Foros: normalmente, las forjas de desarrollo permiten administrar varios foros
de comunicacin donde con la comunidad de usuarios del programa pueden
escribir propuestas y noticar errores.
Las forjas de desarrollo suelen ser accesibles via web, de forma que slo sea necesario un navegador para poder utilizar los diferentes servicios que ofrece. Dependiendo de la forja de desarrollo, se ofrecern ms o menos servicios. Sin embargo, los
expuestos hasta ahora son los que se proporcionan habitualmente. Existen forjas gratuitas en Internet que pueden ser utilizadas para la creacin de un proyecto. Algunas
de ellas:
GNA5 : es una forja de desarrollo creada por la Free Software Foundation de
Francia que soporta, actualmente, repositorios CVS, GNU Arch y Subversion.
Los nuevos proyectos son estudiados cuidadosamente antes de ser autorizados.
Launchpad6 : forja gratuita para proyectos de software libre creada por Canonical Ltd. Se caracteriza por tener un potente sistema de bug tracking y proporcionar herramientas automticas para despliegue en sistemas Debian/Ubuntu.
BitBucket7 : forja de la empresa Atlassian que ofrece repositorios Mercurial y
Git. Permite crear proyectos privados gratuitos pero con lmite en el nmero de
desarrolladores por proyecto.
GitHub8 : forja proporcionada por GitHub Inc. que utiliza repositorios Git. Es
gratuito siempre que el proyecto sea pblico, es decir, pueda ser descargado y
modicado por cualquier usuario de Github sin restriccin.
5 http://gna.org
6 https://launchpad.net/
7 http://bitbucket.org
8 http://github.com
[63]
SourceForge9 : probablemente, una de las forjas gratuitas ms conocidas. Propiedad de la empresa GeekNet Inc., soporta Subversion, Git, Mercurial, Bazaar
y CVS.
Google Code10 : la forja de desarrollo de Google que soporta Git, Mercurial y
Subversion.
Redmine
9 http://sourceforge.net
10 http://code.google.com
C2
Captulo
3.1.
POO
La programacin orientada a objetos tiene como objetivo la organizacin ecaz de programas. Bsicamente, cada componente es un
objeto autocontenido que tiene una
serie de operaciones y de datos o
estado. Este planteamiento permite reducir la complejidad y gestionar grandes proyectos de programacin.
Utilidades bsicas
En esta seccin se realiza un recorrido por los aspectos bsicos de C++, haciendo especial hincapi en aquellos elementos que lo diferencian de otros lenguajes de
programacin y que, en ocasiones, pueden resultar ms complicados de dominar por
aquellos programadores inexpertos en C++.
3.1.1.
Introduccin a C++
[66]
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 losofa 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 eciencia 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 ecientemente
en la industria del videojuego consiste en encontrar el equilibrio adecuado entre eciencia, abilidad y mantenibilidad [27].
3.1.2.
La directiva include de la lnea 3 incluye la biblioteca
<iostream>, la cual soporta
el sistema de E/S de C++. A continuacin, en la 4 el programa le indica al compilador que utilice el espacio de nombres std, en el que se declara la biblioteca estndar de
C++. Un espacio de nombres delimita una zona de declaracin en la que incluir diferentes elementos de un programa. Los espacios de nombres siguen la misma losofa
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 especicar a qu espacio pertenecen.
1 http://www2.research.att.com/~bs/
Dominar C++
Una de las mayores ventajas de
C++ es que es extremadamente potente. Sin embargo, utilizarlo ecientemente es dcil y su curva de
aprendizaje no es gradual.
[67]
En la lnea 10 de hace uso de cout (console output), la sentencia de salida por
consola junto con el operador <<, redireccionando lo que queda a su derecha, es
decir, Por favor, introduzca su nombre..., hacia la salida por consola. A continuacin,
en la siguiente lnea se hace uso de cin (console input) junto con el operador >>
para redirigir la entrada proporcionado por teclado a la variable nombre. Note que
dicha variable es de tipo cadena, un tipo de datos de C++ que se dene como
un array
de caracteres nalizado con el carcter null. Finalmente, en la lnea 12 se saluda al
lector, utilizando adems la sentencia endl para aadir un retorno de carro a la salida
y limpiar el buffer.
3.1.3.
En C++, los tipos de datos bsicos y sus declaraciones siguen un esquema muy
similar al de otros lenguajes de programacin, como por ejemplo Java. En este caso,
existen siete tipos de datos bsicos: carcter, carcter amplio, entero, punto otante,
punto otante de doble precisin, lgico o booleano y nulo. Recuerde que en C++ es
necesario declarar una variable antes de utilizarla para que el compilador conozca el
tipo de datos asociado a la variable. Al igual que ocurre en otros lenguajes de programacin, las variables pueden tener un alcance global o local (por ejemplo en una
funcin).
En el caso particular de los tipos nulos o void, cabe destacar que stos se utilizan
para las funciones con tipo de retorno nulo o como tipo base para punteros a objetos
de tipo desconocido a priori.
C++ permite modicar los tipos char, int y double con los modicadores signed,
unsigned long, y short, con el objetivo de incrementar la precisin en funcin de las
necesidades de la aplicacin. Por ejemplo, el tipo de datos double se puede usar con
el modicador long, y no as con el resto.
C++. Tipado.
C++ es un lenguaje de programacin con tipado esttico, es decir, la
comprobacin de tipos se realiza en
tipo de compilacin y no en tiempo
de ejecucin.
Por otra parte, C++ tambin posibilita el concepto de constante mediante la palabra reservada const, con el objetivo de expresar que un valor no cambia de manera
directa. Por ejemplo, muchos objetos no cambian sus valores despus de inicializarlos. Adems, las constantes conducen a un cdigo ms mantenible en comparacin
con los valores literales incluidos directamente en el cdigo. Por otra parte, muchos
punteros tambin se suelen utilizar solamente para operaciones de lectura y, particularmente, los parmetros de una funcin no se suelen modicar sino que simplemente
se consultan.
El uso de const tambin es ventajoso respecto a la directiva de preprocesado #dene, ya que permite que el compilador aplique la tpica prevencin de errores de tipo.
Esto posibilita detectar errores en tiempo de compilacin y evitar problemas potenciales. Adems, la depuracin es mucho ms sencilla por el hecho de manejar los
nombres simblicos de las constantes.
Desreferencia
El acceso al objeto al que seala un
puntero es una de las operaciones
fundamentales y se denomina indireccin o desreferencia, indistintamente.
C3
[68]
3.1.4.
En el caso de los punteros, el operador unario * es el utilizado para desreferenciarlos y acceder a su contenido, mientras que el operador unario & se utiliza para acceder
a la direccin de memoria de su operando. Por ejemplo,
edadptr = &edad;
En C++ existe una relacin muy estrecha entre los punteros y los arrays, siendo
posible intercambiarlos en la mayora de casos. El siguiente listado de cdigo muestra
un sencillo ejemplo de indexacin de un array mediante aritmtica de punteros.
Listado 3.2: Indexacin de un array
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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;
}
En la inicializacin del bucle for de la lnea 10 se aprecia cmo se asigna la direccin de inicio del array s al puntero p para, posteriormente, indexar el array mediante
p para resaltar en maysculas el contenido del array una vez nalizada la ejecucin
del bucle. Note que C++ permite la inicializacin mltiple.
Indireccin mltiple
Un puntero a puntero es un caso de indireccin mltiple. El primer puntero contiene la direccin
de memoria de otro puntero, mientras que ste contiene la direccin
de memoria de una variable.
[69]
Los punteros son enormemente tiles y potentes. Sin embargo, cuando un puntero
almacena, de manera accidental, valores incorrectos, el proceso de depuracin puede
resultar un autntico quebradero de cabeza. Esto se debe a la propia naturaleza del
puntero y al hecho de que indirectamente afecte a otros elementos de un programa, lo
cual puede complicar la localizacin de errores.
El caso tpico tiene lugar cuando un puntero apunta a algn lugar de memoria que
no debe, modicando 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 [81].
1. No inicializar punteros. En el listado que se muestra a continuacin, p contiene
una direccin desconocida debido a que nunca fue denida. 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 int main () {
2
int edad, *p;
3
4
edad = 23;
5
*p = edad; // p?
6
7
return 0;
8 }
2. Comparar punteros de forma no vlida. La comparacin de punteros es, generalmente, invlida y puede causar errores. En otras palabras, no se deben realizar
suposiciones sobre qu direccin de memoria se utilizar para almacenar los datos,
si siempre ser dicha direccin o si distintos 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.
Listado 3.4: Segundo error tpico. Comparacin incorrecta de punteros.
1 int main () {
2
int s[10];
3
int t[10];
4
int *p, *q;
5
6
p = s;
7
q = t;
8
9
if (p < q) {
10
// ...
11
}
12
13
return 0;
14 }
C3
[70]
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.
#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;
}
Punteros y const
El uso de punteros y const puede
dar lugar a confusin, en funcin
del lugar en el que se site dicha
palabra clave. Recuerde que const
siempre se reere a lo que se encuentra inmediatamente a la derecha. As, int* const p es un puntero
constante, pero no as los datos a los
que apunta.
[71]
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
return 0;
3.1.5.
Referencias y funciones
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 modique (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.
C3
[72]
El siguiente listado de cdigo muestra la tpica funcin swap implementada mediante el uso de referencias. Como se puede apreciar, los parmetros pasados por referencia no hacen uso en ningn momento del operador *, como ocurre con los punteros.
En realidad, el compilador genera automticamente la direccin de los argumentos con
los que se llama a swap, desreferenciando a ambos.
#include <iostream>
using namespace std;
void swap (int &a, int &b);
int main () {
int x = 7, y = 13;
cout << "[" << x << ", " << y << "]" << endl; // Imprime [7, 13].
swap(x, y);
cout << "[" << x << ", " << y << "]" << endl; // Imprime [13, 7].
return 0;
}
void swap (int &a, int &b) {
int aux;
aux = a; // Guarda el valor al que referencia a.
a = b;
// Asigna el valor de b a a.
b = aux; // Asigna el valor de aux a b.
}
[73]
Las funciones tambin pueden devolver referencias. En C++, una de las mayores
utilidades de esta posibilidad es la sobrecarga de operadores. Sin embargo, en el listado que se muestra a continuacin se reeja otro uso potencial, debido a que cuando
se devuelve una referencia, en realidad se est devolviendo un puntero implcito al
valor de retorno. Por lo tanto, es posible utilizar la funcin en la parte izquierda de
una asignacin.
Funciones y referencias
En una funcin, las referencias se
pueden utilizar como parmetros de
entrada o como valores de retorno.
Como se puede apreciar en el siguiente listado, la funcin f devuelve una referencia a un valor en punto otante de doble precisin, en
a la variable global
concreto
valor. La parte importante del cdigo est en la lnea 15 , en la que valor se actualiza
a 7,5, debido a que la funcin devuelve dicha referencia.
Listado 3.8: Retorno de referencias.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
double &f ();
double valor = 10.0;
int main () {
double nuevo_valor;
cout << f() << endl;
nuevo_valor = f();
cout << nuevo_valor << endl;
f() = 7.5;
cout << f() << endl;
return 0;
}
double &f () { return valor; }
C3
[74]
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.1.
Fundamentos bsicos
En el mbito de la POO, las clases representan una manera de asociar datos con
funcionalidad. Los objetos son las instancias especcas de una clase, de manera que
cada una tiene sus propios datos pero todas ellas comparten la misma funcionalidad a
nivel de clase.
La parte de datos en una clase de C++ no diere de una estructura en C. Sin embargo, C++ ofrece tres niveles de acceso a los datos: pblicos, privados o protegidos.
Por defecto, los miembros de una clase son privados, mientras que en una estructura
son pblicos.
Debido a que la mayora de los objetos requieren una inicializacin de su estado, C++ permite inicializar los objetos cuando estos son creados mediante el uso de
constructores. Del mismo modo, C++ contempla el concepto de destructor para contemplar la posibilidad de que un objeto realice una serie de operaciones antes de ser
destruido. El siguiente listado de cdigo muestra la especicacin de una clase en C++
con los miembros de dicha clase, su visibilidad, el constructor, el destructor y otras
funciones.
Destruccin de objetos
Recuerde que los objetos creados
dentro de un bloque se destruyen
cuando dicho bloque se abandona
por el ujo del programa. Por el
contrario, los objetos globales se
destruyen cuando el programa naliza su ejecucin.
[75]
C3
3.2. Clases
class Figura
{
public:
Figura (double i, double j);
~Figura ();
void setDim (double i, double j);
double getX () const;
double getY () const;
protected:
double _x, _y;
};
Note cmo las variables de clase se denen 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 ~.
Paso por referencia
Recuerde utilizar parmetros por
referencia const para minimizar el
nmero de copias de los mismos.
El resto de funciones sirven para modicar y acceder al estado de los objetos instanciados a partir de dicha clase. Note el uso del modicador const en las funciones
de acceso getX() y getY(), con el objetivo de informar de manera explcita al compilador de que dichas funciones no modican el estado de los objetos. A continuacin, se
muestra la implementacin de las funciones denidas en la clase Figura.
Recuerde estructurar adecuademente su cdigo y seguir un convenio de nombrado que facilite su mantenibilidad. Si se integra en un proyecto activo, procure seguir el convenio previamente adoptado.
Uso de inline
El modicador inline se suele incluir despus de la declaracin de la
funcin para evitar lneas de cdigo
demasiado largas (siempre dentro
del archivo de cabecera). Sin embargo, algunos compiladores obligan a incluirlo en ambos lugares.
Antes de continuar discutiendo ms aspectos de las clases, resulta interesante introducir brevemente el concepto de funciones en lnea (inlining), una tcnica que
puede reducir la sobrecarga implcita en las llamadas a funciones. Para ello, slo es
necesario incluir el modicador inline delante de la declaracin de una funcin. Esta tcnica permite obtener exactamente el mismo rendimiento que el acceso directo
a una variable sin tener que desperdiciar tiempo en ejecutar la llamada a la funcin,
interactuar con la pila del programa y volver de dicha funcin.
Las funciones en lnea no se pueden usar indiscriminadamente, ya que pueden
degradar el rendimiento de la aplicacin fcilmente. En primer lugar, el tamao del
ejecutable nal se puede disparar debido a la duplicidad de cdigo. As mismo, la
cach de cdigo tambin puede hacer que dicho rendimiento disminuya debido a las
continuas penalizaciones asociadas a incluir tantas funciones en lnea. Finalmente, los
tiempos de compilacin se pueden incrementar en grandes proyectos.
[76]
#include "Figura.h"
Figura::Figura
(double i, double j)
{
_x = i;
_y = j;
}
Figura::~Figura ()
{
}
void
Figura::setDim
(double i, double j)
{
_x = i;
_y = j;
}
double
Figura::getX () const
{
return _x;
}
double
Figura::getY () const
{
return _y;
}
Una buena regla para usar de manera adecuada el modicador inline consiste
en evitar su uso hasta prcticamente completar el desarrollo de un proyecto. A
continuacin, se puede utilizar alguna herramienta de proling para detectar
si alguna funcin sencilla est entre las ms utilidas. Este tipo de funciones
son candidatas potenciales para modicarlas con inline y, en consecuencia,
elementos para mejorar el rendimiento del programa.
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.
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 #include <iostream>
[77]
2
3
4
5
6
7
8
9
10
11
12
13
#include "Figura.h"
using namespace std;
int main () {
Figura *f1;
f1 = new Figura (1.0, 0.5);
cout << "[" << f1->getX() << ", " << f1->getY() << "]" << endl;
3.2.2.
delete f1;
return 0;
C3
3.2. Clases
[78]
Como se puede apreciar, existe una llamada al constructor al crear a (lnea 24 ) y
dos llamadas al destructor. Como se ha comentado antes, cuando un objeto se pasa a
una funcin, entonces se crea una copia del mismo, la cual se destruye cuando naliza
la ejecucin de la funcin. Ante esta situacin surgen dos preguntas: i) se realiza una
llamada al constructor? y ii) se realiza una llamada al destructor?
En realidad, lo que ocurre cuando se pasa un objeto a una funcin es que se llama
al constructor de copia, cuya responsabilidad consiste en denir cmo se copia un
objeto. Si una clase no tiene un constructor de copia, entonces C++ proporciona uno
por defecto, el cual crea una copia bit a bit del objeto. En realidad, esta decisin es
bastante lgica, ya que el uso del constructor normal para copiar un objeto no generara
el mismo resultado que el estado que mantiene el objeto actual (generara una copia
con el estado inicial).
Sin embargo, cuando una funcin naliza y se ha de eliminar la copia del objeto,
entonces se hace uso del destructor debido a que la copia se encuentra fuera de su
mbito local. Por lo tanto, en el ejemplo anterior se llama al destructor tanto para la
copia como para el argumento inicial.
[79]
C3
3.2. Clases
#include <iostream>
using namespace std;
class A {
int _valor;
public:
A(int valor): _valor(valor) {
cout << "Construyendo..." << endl;
}
~A() {
cout << "Destruyendo..." << endl;
}
int getValor () const {
return _valor;
}
};
void mostrar (A a) {
cout << a.getValor() << endl;
}
int main () {
A a(7);
mostrar(a);
return 0;
}
El constructor de copia representa a un tipo especial de sobrecarga del constructor y se utiliza para gestionar de manera adecuada la copia de objetos. Como se ha
discutido anteriormente, la copia exacta de objetos puede producir efectos no deseables, especialmente cuando se trata con asignacin dinmica de memoria en el propio
constructor.
Recuerde que C++ contempla dos tipos de situaciones distintas en las que el valor
de un objeto se da a otro: la asignacin y la inicializacin. Esta segunda se pueda dar
de tres formas distintas:
Cuando un objeto inicializa explcitamente a otro, por ejemplo en una declaracin.
Cuando la copia de un objeto se pasa como parmetro en una funcin.
[80]
Es importante resaltar que el constructor de copia slo se aplica en las inicializaciones y no en las asignaciones. El siguiente listado de cdigo muestra un ejemplo de
implementacin del constructor de copia, en el que se gestiona adecuadamente la asignacin de memoria dinmica en el constructor. Dicho ejemplo se basa en el discutido
anteriormente sobre el uso de paso de objetos por valor.
Como se puede apreciar, el constructor normal hace una reserva dinmica de memoria. Por lo tanto, el constructor de copia se utiliza para que, al hacer la copia, se
reserve nueva memoria y se asigne el valor adecuado a la variable de clase.
#include <iostream>
using namespace std;
class A {
int *_valor;
public:
A(int valor);
// Constructor.
A(const A &obj); // Constructor de copia.
~A();
// Destructor.
int getValor () const { return *_valor;}
};
A::A (int valor) {
cout << "Construyendo..." << endl;
_valor = new int;
*_valor = valor;
}
A::A (const A &obj) {
cout << "Constructor copia..." << endl;
_valor = new int;
*_valor = obj.getValor();
}
A::~A () {
cout << "Destruyendo..." << endl;
delete _valor;
}
void mostrar (A a) {
cout << a.getValor() << endl;
}
int main () {
A a(7);
mostrar(a);
return 0;
}
[81]
7
Destruyendo...
Destruyendo...
Finalmente, antes de pasar a la seccin de sobrecarga de operadores, C++ contempla, al igual que en otros lenguajes de programacin, el uso de this como el puntero al
objeto que invoca una funcin miembro. Bsicamente, dicho puntero es un parmetro
implcito a todas las funciones miembro de una clase.
3.2.3.
Sobrecarga de operadores
class Point3D {
public:
Point3D ():
_x(0), _y(0), _z(0) {}
Point3D (int x, int y, int z):
_x(x), _y(y), _z(z) {}
Point3D operator+ (const Point3D &op2);
private:
int _x, _y, _z;
};
Point3D
Point3D::operator+
(const Point3D &op2) {
Point3D resultado;
resultado._x = this->_x + op2._x;
resultado._y = this->_y + op2._y;
resultado._z = this->_z + op2._z;
return resultado;
}
C3
3.2. Clases
[82]
#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);
}
[83]
El constructor de copia se dene entre las lneas 18 y 21 , reservando una nueva
regin de memoria para el contenido
y copiando el mismo en la variable
de _valor
miembro. Por otra parte, las lneas 23-26 muestran la implementacin de la funcin
set, que modica el contenido de dicha variable miembro.
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 denido. 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 nalizar el programa.
Listado 3.18: Sobrecarga del operador de asignacin (cont.)
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;
}
C3
3.2. Clases
[84]
Finalmente, resulta especialmente relevante destacar que C++ permite la sobrecarga de cualquier operador, a excepcin de new, delete, ->, ->* y el operador coma,
que requieren otro tipo de tcnicas. El resto de operadores se sobrecargan del mismo
modo que los discutidos en esta seccin.
3.3.
Herencia y polimorsmo
3.3.1.
Polimorsmo
El polimorsmo se suele denir como una interfaz, mltiples mtodos
y representa una de los aspectos clave de la POO.
Herencia
El siguiente listado de cdigo muestra la clase base Vehculo que, desde un punto
de vista general, dene 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 class Vehiculo {
2
int _ruedas;
// Privado. No accesible en clases derivadas.
3
int _pasajeros;
4
5 public:
6
void setRuedas (int ruedas) {_ruedas = ruedas;}
7
int getRuedas () const {return _ruedas;}
8
void setPasajeros (int pasajeros) {_pasajeros = pasajeros;}
9
int getPasajeros () const {return _pasajeros;}
10 };
La clase base anterior se puede extender para denir coches con una nueva caracterstica propia de los mismos, como se puede apreciar en el siguiente listado.
Herencia y acceso
El modicador de acceso cuando se
usa herencia es opcional. Sin embargo, si ste se especica 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.
[85]
C3
#include <iostream>
#include "Vehiculo.cpp"
using namespace std;
class Coche : public Vehiculo {
int _PMA;
public:
void setPMA (int PMA) {_PMA = PMA;}
int getPMA () const {return _PMA;}
void mostrar () const {
cout << "Ruedas: " << getRuedas() << endl;
cout << "Pasajeros: " << getPasajeros() << endl;
cout << "PMA: " << _PMA << endl;
}
};
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 denir 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.
Listado 3.22: Clase derivada Coche. Acceso protegido
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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;
[86]
19
cout << "PMA: " << _PMA << endl;
20
}
21 };
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 modicador de acceso protected, entonces todos los miembros pblicos y protegidos de dicha clase se heredan
como miembros protegidos en la clase derivada.
Constructores y destructores
Cuando se hace uso de herencia y se denen 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 especicarse aquellos parmetros que se deseen utilizar para llamar al constructor de la
clase base.
3.3.2.
Herencia mltiple
C++ posibilita la herencia mltiple, es decir, permite que una clase derivada herede
de dos o ms clases base. Para ejemplicar la potencia de la herencia mltiple, a
continuacin se plantea un problema que se abordar con distintos enfoques con el
objetivo de obtener una buena solucin de diseo [27].
Suponga que es necesario disear la clase ObjetoJuego, la cual se utilizar como
clase base para distintas entidades en un juego, como los enemigos, las cmaras, los
items, etc. En concreto, es necesario que todos los objetos del juego soporten funcionalidad relativa a la recepcin de mensajes y, de manera simultnea, dichos objetos
han de poder relacionarse como parte de una estructura de rbol.
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.
[87]
El enfoque todo en uno
Una primera opcin de diseo podra consistir en aplicar un enfoque todo en uno,
es decir, implementar los requisitos previamente comentados en la propia clase ObjetoJuego, aadiendo la funcionalidad de recepcin de mensajes y la posibilidad de
enlazar el objeto en cualquier parte del rbol a la propia clase.
Aunque la simplicidad de esta aproximacin es su principal ventaja, en general
aadir todo lo que se necesita en una nica clase no es la mejor decisin de diseo. Si
se utiliza este enfoque para aadir ms funcionalidad, la clase crecer en tamao y en
complejidad cada vez que se integre un nuevo requisito funcional. As, una clase base
que resulta fundamental en el diseo de un juego se convertir en un elemento difcil
de utilizar y de mantener. En otras palabras, la simplicidad a corto plazo se transforma
en complejidad a largo plazo.
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
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.
Uso de la herencia
Recuerde utilizar la herencia con
prudencia. Un buen truco consiste
en preguntarse si la clase derivada
es un tipo particular de la clase base.
Una posible solucin a este problema consiste en exponer los propios objetos en
lugar de envolverlos con llamadas a funciones miembro. Este planteamiento simplica el mantenimiento pero tiene la desventaja de que proporciona ms informacin
de la realmente necesaria en la clase ObjetoJuego. Si adems, posteriormente, es necesario modicar la implementacin de dicha clase con propsitos de incrementar la
eciencia, entonces habra que modicar todo el cdigo que haga uso de la misma.
C3
[88]
class ObjetoJuego {
public:
bool recibirMensaje (const Mensaje &m);
ObjetoJuego* getPadre ();
ObjetoJuego* getPrimerHijo ();
// ...
private:
ReceptorMensajes *_receptorMensajes;
NodoArbol *_nodoArbol;
};
inline bool recibirMensaje (const Mensaje &m) {
return _receptorMensajes->recibirMensaje(m);
}
inline ObjetoJuego* getPadre () {
return _nodoArbol->getPadre();
}
inline ObjetoJuego* getPrimerHijo () {
return _nodoArbol->getPrimerHijo();
}
Objeto
Juego
Receptor
Mensajes
Receptor
Mensajes
Nodo
rbol
Nodo
rbol
Receptor
Mensajes
Nodo
rbol
Objeto
Juego
Objeto
Juego
(a)
(b)
Figura 3.2: Distintas soluciones de diseo para el problema de la clase ObjetoJuego. (a) Uso de agregacin.
(b) Herencia simple. (c) Herencia mltiple.
(c)
[89]
Aunque este planteamiento es perfectamente funcional, el diseo no es adecuado ya que resulta bastante lgico pensar que ArbolNodo no es un tipo especial de
ReceptorMensajes. Si no es as, entonces no debera utilizarse herencia. Simple y llanamente. Del mismo modo, la relacin inversa tampoco es lgica.
Enfoque basado en herencia mltiple
La herencia mltiple representa la solucin idnea al problema planteado. Realmente, la herencia mltiple funciona como la herencia simple, pero posibilita que una
clase derivada herede de dos clases base. En este caso particular, ObjetoJuego podra
heredar de ReceptorMensajes y de ArbolNodo simultneamente. As, la primera clase
tendra de manera automtica la interfaz, las variables miembro y la funcionalidad de
las otras dos clases.
Listado 3.24: Clase ObjetoJuego. Herencia mltiple
1 class ObjetoJuego: public ReceptorMensajes, public NodoArbol {
2 public:
3
// Funcionalidad necesaria.
4 };
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.
C3
[90]
3.3.3.
Como se introdujo al principio de la seccin 3.3, en C++ el polimorsmo est soportado tanto en tiempo de compilacin como en tiempo de ejecucin. La sobrecarga
de operadores y de funciones son ejemplos de polimorsmo en tiempo de compilacin, mientras que el uso de clases derivadas y de funciones virtuales posibilitan el
polimorsmo en tiempo de ejecucin. Antes de pasar a discutir las funciones virtuales, se introducir el concepto de puntero a la clase base como fundamento bsico del
polimorsmo en tiempo de ejecucin.
El puntero a la clase base
En trminos generales, un puntero de un determinado tipo no puede apuntar a un
objeto de otro tipo distinto. Sin embargo, los punteros a las clases bases y derivadas
representan la excepcin a esta regla. En C++, un puntero de una determinada clase
base se puede utilizar para apuntar a objetos de una clase derivada a partir de dicha
clase base.
En el listado de cdigo 3.25 se retoma el ejemplo de uso de herencia entre las
clases Vehculo y Coche para mostrar
el uso de un puntero a una clase base (Vehculo).
Como se puede apreciar en la lnea 11 , el puntero de tipo base Vehculo se utiliza para
apuntar a un objeto de tipo
derivado Coche, para, posteriormente, acceder al nmero
de pasajeros en la lnea 12 .
Listado 3.25: Manejo de punteros a clase base
1 #include "Coche.cpp"
2
3 int main () {
4
Vehiculo *v; // Puntero a objeto de tipo vehculo.
5
Coche c;
// Objeto de tipo coche.
6
7
c.setRuedas(4);
// Se establece el estado de c.
8
c.setPasajeros(7);
9
c.setPMA(1885);
10
11
v = &c;
// v apunta a un objeto de tipo coche.
12
cout << "Pasajeros: " << v->getPasajeros() << endl;
13
14
return 0;
15 }
Flexibilidad en C++
El polimorsmo en C++ es un arma
muy poderosa y, junto con la herencia, permite disear e implementar
programas complejos.
[91]
C3
Una funcin virtual es una funcin declarada como virtual en la clase base y redenida 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.
Listado 3.27: Uso bsico de funciones virtuales
1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
class Base {
public:
virtual void imprimir () const { cout << "Soy Base!" << endl; }
};
class Derivada1 : public Base {
public:
[92]
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
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;
Sobrecarga/sobreescrit.
Cuando una funcin virtual se redene en una clase derivada, la funcin se sobreescribe. Para sobrecargar una funcin, recuerde que el nmero de parmetros y/o sus tipos
han de ser diferentes.
[93]
Funciones virtuales puras y clases abstractas
Si una funcin virtual no se redene en la clase derivada, entonces se utiliza la
funcin denida en la clase base. Sin embargo, en determinadas situaciones no tiene
sentido denir una funcin virtual en una clase base debido a que semnticamente no
es correcto. El escenario tpico se produce cuando existe una clase base, asociada a
un concepto abstracto, para la que no pueden existir objetos. Por ejemplo, una clase
Figura slo tiene sentido como base de alguna clase derivada.
En estos casos, es posible implementar las funciones virtuales de la clase base de
manera que generen un error, ya que su ejecucin carece de sentido en este tipo de clases que manejan conceptos abstractos. Sin embargo, C++ proporciona un mecanismo
para tratar este tipo de situaciones: el uso de funciones virtuales puras. Este tipo de
funciones virtuales permite la denicin de clases abstractas, es decir, clases a partir
de las cuales no es posible instanciar objetos.
El siguiente listado de cdigo muestra un ejemplo en el que se dene la clase
abstracta Figura, como base para la denicin de guras concretos, como el crculo.
Note como la transformacin de una funcin virtual en pura se consigue mediante el
especicador =0.
Fundamentos POO
La encapsulacin, la herencia y el
polimormos representan los pilares fundamentales de la programacin orientada a objetos.
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 dene 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 denirla. En caso contrario,
el compilador generar un error. De hecho, si una funcin virtual pura no se dene en
una clase derivada, entonces dicha funcin virtual sigue siendo pura y, por lo tanto, la
clase derivada es tambin una clase abstracta.
Listado 3.28: Uso bsico de funciones virtuales puras
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 <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;
C3
[94]
33 }
3.4.
Plantillas
3.4.1.
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;
};
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 sobre la que se
ejecutar el juego en cuestin.
[95]
C3
3.4. Plantillas
<MiClase>
<MiClase>
<MiClase>
<MiClase>
(a)
Elemento
Lista
MiClase
(b)
Elemento
Lista
Elemento
Lista
<MiClase>
<MiClase>
(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.
[96]
La gura 3.3 muestra de manera grca las distintas opciones discutidas hasta
ahora en lo relativo a la implementacin de una lista que permita el tratamiento de
datos genricos.
3.4.2.
#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.
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
otante.
[97]
Las plantillas de funciones siguen la misma idea que las plantillas de clases aplicndolas a las funciones. Obviamente, la principal diferencia con respecto a las plantillas a clases es que no necesitan instanciarse. El siguiente listado de cdigo muestra
un ejemplo sencillo de la clsica funcin swap para intercambiar el contenido de dos
variables. Dicha funcin puede utilizarse con enteros, valores en punto otante, 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 otante, ya que se producir un error en tiempo de compilacin.
Listado 3.31: Ejemplo de uso plantillas de funciones
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Uso de plantillas
Las plantillas son una herramienta
excelente para escribir cdigo que
no dependa de un tipo de datos especco.
#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;
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 especicar la funcionalidad de la propia lista.
Como se puede apreciar en el siguiente listado de cdigo, las dos clases estn
denidas para poder utilizar cualquier tipo de dato y, adems, la funcionalidad de la
lista es totalmente independiente del su contenido.
Listado 3.32: Uso de plantillas para implementar listas
1 template<class T>
2 class NodoLista {
3 public:
4
NodoLista (T datos);
5
T & getDatos ();
6
NodoLista * siguiente ();
C3
3.4. Plantillas
[98]
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private:
T _datos;
};
template<class T>
class Lista {
public:
NodoLista<T> getCabeza ();
void insertarFinal (T datos);
// Resto funcionalidad...
private:
NodoLista<T> *_cabeza;
};
3.4.3.
Las plantillas de C++ representan un arma muy poderosa para implementar cdigo
gnerico que se pueda utilizar con distintos tipos de datos. Sin embargo, al igual que
ocurre con la herencia, hay que ser especialmente cuidadoso a la hora de utilizarlas,
debido a que un uso inadecuado puede generar problemas y retrasos en el desarrollo
de software. A continuacin se enumeran las principales desventajas que plantea el
uso de plantillas [27]:
Complejidad, debido a la integracin de nueva nomenclatura que puede dicultar la legibilidad del cdigo. Adems, el uso de plantillas hace que la depuracin
de cdigo sea ms difcil.
Dependencia, ya que el cdigo de la plantilla ha de incluirse en un chero de
cabecera para que sea visible por el compilador a la hora de instanciarlo. Este
planteamiento incrementa el acoplamiento entre clases. Adems, el tiempo de
compilacin se ve afectado.
Duplicidad de cdigo, debido a que si, por ejemplo, se crea una lista con un
nuevo tipo de datos, el compilador ha de crear una nueva clase de lista. Es
decir, todas las funciones y variables miembro se duplican. En el desarrollo de
videojuegos, este inconveniente es generalmente asumible debido a la magnitud
de los proyectos.
Soporte del compilador, ya que las plantillas no existen como una solucin
plenamente estandarizada, por lo que es posible, aunque poco probable, que
algunos compiladores no las soporten.
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++.
[99]
3.5.
Freezing issues
Aunque el desarrollo de videojuegos comerciales madura ao a ao,
an hoy en da es bastante comn
encontrar errores y bugs en los mismos. Algunos de ellos obligan incluso a resetear la estacin de juegos por completo.
Manejo de excepciones
3.5.1.
Alternativas existentes
C3
[100]
Hasta ahora, los enfoques comentados tienen una serie de desventajas importantes.
En este contexto, las excepciones se posicionan como una alternativa ms adecuada
y prctica. En esencia, el uso de excepciones permite que cuando un programa se
tope con una situacin inesperada se arroje una excepcin. Este hecho tiene como
consecuencia que el ujo de ejecucin salte al bloque de captura de excepciones ms
cercano. Si dicho bloque no existe en la funcin en la que se arroj la excepcin,
entonces el programa gestionar de manera adecuada la salida de dicha funcin (destruyendo los objetos vinculados) y saltar a la funcin padre con el objetivo de buscar
un bloque de captura de excepciones.
Este proceso se realiza recursivamente hasta encontrar dicho bloque o llegar a
la funcin principal delegando en el cdigo de manejo de errores por defecto, que
tpicamente nalizar la ejecucin del programa y mostrar informacin por la salida
estndar o generar un chero de log.
Los bloques de tratamiento de excepciones ofrecen al desarrollador la exibilidad de hacer lo que desee con el error, ya sea ignorarlo, tratar de recuperarse del
mismo o simplemente informar sobre lo que ha ocurrido. Este planteamiento facilita enormemente la distincin entre distintos tipos de errores y, consecuentemente, su
tratamiento.
Recuerde que despus de la ejecucin de un bloque de captura de excepciones, la ejecucin del programa continuar a partir de este punto y no desde
donde la excepcin fue arrojada.
3.5.2.
Excepciones en C++
Excepciones estndar
C++ maneja una jerarqua de excepciones estndar para tipos generales, como por ejemplo logic_error o runtime_error, o aspectos ms especcos, como por ejemplo out_of_range o bad_alloc. Algunas de las funciones de la biblioteca estndar de C++ lanzan algunas de estas excepciones.
[101]
4
5 int main () {
6
try {
7
int *array = new int[1000000];
8
}
9
catch (bad_alloc &e) {
10
cerr << "Error al reservar memoria." << endl;
11
}
12
13
return 0;
14 }
En particular, el cdigo dene una excepcin general en las lneas 4-10 mediante
la denicin 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 instancia de dicha excepcin, denida en la lnea 21 , cuando el usuario
introduce un valor numrico que no est en el rango [1, 10]. Posteriormente, dicha
excepcin se captura en las lneas 24-26 .
Listado 3.34: Excepcin denida por el usuario
Excepciones y clases
Las excepciones se modelan exactamente igual que cualquier otro tipo de objeto y la denicin de clase
puede contener tanto variables como funciones miembro.
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
#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;
C3
[102]
MiExcepcin
MiExcepcinMemoria
MiExcepcinIO
MiExcepcinMatemtica
[103]
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.
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.
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.
3.5.3.
Adems de conocer cmo utilizar las sentencias relativas al tratamiento de excepciones, un desarrollador ha de conocer cmo utilizarlas de manera adecuada para
evitar problemas potenciales, como por ejemplo no liberar memoria que fue reservada
previamente al lanzamiento de una excepcin. En trminos generales, este problema
se puede extrapolar a cmo liberar un recurso que se adquiri previamente a la generacin de un error.
El siguiente listado de cdigo muestra cmo utilizar excepciones para liberar correctamente los recursos previamente reservados en una funcin relativa a la carga
de texturas a partir de la ruta de una imagen.
Como se puede apreciar, en la funcin cargarTextura se reserva memoria para
el manejador del archivo y para el propio objeto de tipo textura. Si se generase una
excepcin dentro del bloque try, entonces se ejecutara el bloque catch genrico que
se encarga de liberar los dos recursos previamente mencionados. As mismo, todos los
recursos locales de la propia funcin se destruirn tras salir de la misma.
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.
C3
[104]
Como se puede apreciar, no es necesario incluir ningn tipo de cdigo de manejo de excepciones, ya que la propia funcin se encarga de manera implcita gracias
al enfoque planteado. Recuerde que una vez que el ujo del programa abandone la
funcin, ya sea de manera normal o provocado por una excepcin, todo los recursos
habrn sido liberados de manera adecuada.
Finalmente, es importante destacar que este tipo de punteros inteligentes se pueden
utilizar en otro tipo de situaciones especcas, como por ejemplo los constructores.
Recuerde que, si se genera una excepcin en un constructor, no es posible devolver
ningn cdigo de error. Adems, si ya se reserv memoria en el constructor, entonces
[105]
se producir el clsico memory leak, es decir, la situacin en la que no se libera una
porcin de memoria que fue previamente reservada. En estos casos, el destructor no se
ejecuta ya que, despus de todo, el constructor no naliz correctamente su ejecucin.
La solucin pasa por hacer uso de este tipo de punteros.
El caso de los destructores es menos problemtico, ya que es lgico suponer
que nadie har uso de un objeto que se va a destruir, incluso cuando se genere una
excepcin dentro del propio destructor. Sin embargo, es importante recordar que el
destructor no puede lanzar una excepcin si el mismo fue llamado como consecuencia
de otra excepcin.
3.5.4.
Plataformas HW
En el mbito del desarrollo de videojuegos, el uso de excepciones
est estrechamente con la plataforma HW nal sobre la que se ejecutar el juego en cuestin, debido al
impacto en el rendimiento que tienen las mismas.
En el mbito particular del tratamiento de excepciones en el desarrollo de videojuegos, las excepciones se deberan utilizar para modelar situaciones realmente excepcionales [27], es decir, situaciones que nunca ocurren o que ocurren con poqusima
frecuencia. Cuando se lanza una excepcin se pone en marcha un proceso complejo
compuesto de diversas operaciones, como por ejemplo la bsqueda del bloque de manejo de excepciones ms cercano, la modicacin de la pila, la destruccin de objetos
y la transferencia del control del programa al bloque catch.
Este proceso es lento y afectara enormemente al rendimiento del programa. Sin
embargo, en el mbito del desarrollo de videojuegos esta situacin no resulta tan crtica, debido a que el uso de excepciones se limita, generalmente, a casos realmente
excepcionales. En otras palabras, las excepciones no se utilizan para detectar que, por
ejemplo, un enemigo ha cado al agua, sino para modelar aspectos crticos como que
el sistema se est quedando sin memoria fsica. Al nal, este tipo de situaciones puede
conducir a una eventual parada del sistema, por lo que el impacto de una excepcin
no resulta tan importante.
En este contexto, las consolas de videojuegos representan el caso ms extremo,
ya que normalmente tienen los recursos acotados y hay que ser especialmente cuidadoso con el rendimiento de los juegos. El caso ms representativo es el tamao de
la memoria principal, en el que el impacto de utilizar excepciones puede ser desastroso. Sin embargo, en un PC este problema no es tan crtico, por lo que el uso de
excepciones puede ser ms recomendable. Algunos autores recomiendan no hacer uso
de excepciones [42], sino de cdigos de error, en el desarrollo para consolas de videojuegos, debido a su limitada capacidad de memoria.
Tambin es importante reexionar sobre el impacto del uso de las excepciones
cuando no se lanzan, es decir, debido principalmente a la inclusin de sentencias trycatch. En general, este impacto est vinculado al compilador y a la forma que ste
tiene para tratar con esta situacin. Otro aspecto relevante es dnde se usa este tipo
de sentencias. Si es en fases del juego como la carga inicial de recursos, por ejemplo
mapas o modelos, se puede asumir perfectamente la degradacin del rendimiento.
Por el contrario, si dichas sentencias se van a ejecutar en un mdulo que se ejecuta
continuamente, entonces el rendimiento se ver afectado enormemente.
C3
[106]
El uso de excepciones puede coexistir perfectamente con el uso de valores de retorno, aunque es importante tener claro cundo utilizar un planteamiento y cundo
utilizar otro. Por ejemplo, en el mbito del desarrollo de videojuegos, el uso de valores de retorno es una solucin ms prctica y limpia si lo que se desea es simplemente
conocer si una funcin termin su ejecucin de manera adecuada o no, independientemente de los tipos de errores que puedan producirse.
Captulo
Patrones de Diseo
Cleto Martn Angelina
Francisco Moya Fernndez
uando nos enfrentamos al diseo de un programa informtico como un videojuego, no es posible abordarlo por completo. El proceso de diseo de una
aplicacin suele ser iterativo y en diferentes etapas de forma que se vaya renando con el tiempo. El diseo perfecto y a la primera es muy difcil de conseguir.
La tarea de disear aplicaciones es compleja y, con seguridad, una de las ms
importantes y que ms impacto tiene no slo sobre el producto nal, sino tambin
sobre su vida futura. En el diseo de la aplicacin es donde se denen las estructuras
y entidades que se van a encargar de resolver el problema modelado, as como sus
relaciones y sus dependencias. Cmo de bien denamos estas entidades y relaciones
inuir, en gran medida, en el xito o fracaso del proyecto y en la viabilidad de su
mantenimiento.
El diseo, por tanto, es una tarea capital en el ciclo de vida del software. Sin
embargo, no existe un procedimiento sistemtico y claro sobre cmo crear el mejor
diseo para un problema dado. Podemos utilizar metodologas, tcnicas y herramientas que nos permitan renar nuestro diseo. La experiencia tambin juega un papel
importante. Sin embargo, el contexto de la aplicacin es crucial y los requisitos, que
pueden cambiar durante el proceso de desarrollo, tambin.
Un videojuego es un programa con una componente muy creativa. Adems, el
mercado de videojuegos se mueve muy deprisa y la adaptacin a ese medio cambiante es un factor determinante. En general, los diseos de los programas deben ser
escalables, extensibles y que permitan crear componentes reutilizables.
Esta ltima caracterstica es muy importante ya que la experiencia nos hace ver que
al construir una aplicacin se nos presentan situaciones recurrentes y que se asemejan
a situaciones pasadas. Es el deja v en el diseo: cmo solucion esto?. En esencia,
muchos componentes pueden modelarse de formas similares.
107
[108]
En este captulo, se describen algunos patrones de diseo que almacenan este conocimiento experimental de diseo procedente del estudio de aplicaciones y de los
xitos y fracasos de casos reales. Bien utilizados, permiten obtener un mejor diseo
ms temprano.
4.1.
Introduccin
Denicin
En [38], 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.
4.1. Introduccin
[109]
1 Tambin conocido como The Gang of Four (GoF) (la Banda de los Cuatro) en referencia a los autores
del mismo. Sin duda se trata de un famoso libro en este rea, al que no le faltan detractores.
C4
4.1.1.
[110]
4.1.2.
Tipos de patrones
De entre los diferentes criterios que se podran adoptar para clasicar los diferentes
patrones de diseo, uno de los ms aceptados es el mbito de diseo donde tienen
aplicacin. De esta forma, se denen tres categoras fundamentales:
Patrones de creacin: se trata de aquellos que proporcionan una solucin relacionada con la construccin de clases, objetos y otras estructuras de datos. Por
ejemplo, patrones como Abstract Factory, Builder y otros ofrecen mecanismos
de creacin de instancias de objetos y estructuras escalables dependiendo de las
necesidades.
Patrones estructurales: este tipo de patrones versan sobre la forma de organizar
las jerarquas de clases, las relaciones y las diferentes composiciones entre objetos para obtener un buen diseo en base a unos requisitos de entrada. Patrones
como Adapter, Facade o Flyweight son ejemplos de patrones estructurales.
Patrones de comportamiento: las soluciones de diseo que proporcionan los
patrones de comportamiento estn orientadas al envo de mensajes entre objetos y cmo organizar ejecuciones de diferentes mtodos para conseguir realizar
algn tipo de tarea de forma ms conveniente. Algunos ejemplos son Visitor,
Iterator y Observer.
Algunos profesionales, como Jeff Atwood, critican el uso excesivo de patrones. Argumentan que es ms importante identicar bien las responsabilidades de cada entidad que el propio uso del patrn. Otros se plantean si el
problema no est realmente en los propios lenguajes de programacin, que
no proporcionan las herramientas semnticas necesarias.
4.2.
Patrones de creacin
En esta seccin se describen algunos patrones que ayudan en el diseo de problemas en los que la creacin de instancias de diferentes tipos es el principal problema.
4.2.1.
Singleton
El patrn singleton se suele utilizar cuando se requiere tener una nica instancia
de un determinado tipo de objeto.
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 [38]) consideran que es un antipatrn, es decir, una
mala solucin a un problema de diseo.)
[111]
/* 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.
C4
Problema
[112]
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 cheros, render de grcos, etc.
Estructuras que representan la conguracin 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.
4.2.2.
Abstract Factory
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 especco 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.
[113]
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 gura 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 dene una factora abstracta que ser la que utilice el cliente
(Game) para crear los diferentes objetos. CharFactory es una factora que slo
dene 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.
En denitiva, el patrn Abstract Factory recomienda crear las siguientes entidades:
Factora abstracta que dena una interfaz para que los clientes puedan crear los
distintos tipos de objetos.
Factoras concretas que realmente crean las instancias nales.
Implementacin
Basndonos en el ejemplo anterior, el objeto Game sera el encargado de crear los
diferentes personajes utilizando una factora abstracta. El siguiente fragmento de cdigo muestra cmo la clase Game recibe una factora concreta (utilizando polimorsmo)
y la implementacin del mtodo que crea los soldados.
C4
[114]
/* ... */
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;
}
/* 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.
[115]
es necesaria la creacin de varias instancias de objetos para tener el sistema
congurado.
cuando 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 es un poco problemtico. En nuestro ejemplo, si
quisiramos aadir un nuevo tipo de soldado deberamos modicar 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.
4.2.3.
Factory Method
El patrn Factory Method se basa en la denicin de una interfaz para crear instancias de objetos y permite a las subclases decidir cmo se crean dichas instancias
implementando un mtodo determinado.
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.
C4
[116]
Solucin
La gura 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.
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 dene una interfaz con mtodos de construccin de objetos que son implementados por las subclases.
4.2.4.
Prototype
[117]
4.3.
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 eciente para resolver un determinado
problema.
C4
Solucin
[118]
4.3.1.
Composite
Por otro lado, hay objetos hoja que no contienen a ms objetos, como es el caso
de Clock.
Consideraciones
Al utilizar este patrn, se debe tener en cuenta las siguientes consideraciones:
Una buena estrategia para identicar la situacin en la que aplicar este patrn
es cuando tengo un X y tiene varios objetos X.
[119]
La estructura generada es muy exible 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.
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.3.2.
Decorator
Tambin conocido como Wrapper, el patrn Decorator sirve para aadir y/o modicar 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 gura 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.
Listado 4.4: Decorator
1
2
3
4
5
6
7
class Firearm {
public:
virtual float noise() const = 0;
virtual int bullets() const = 0;
};
class Rifle : public Firearm {
C4
[120]
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;
[121]
En cada momento, qu valores se imprimen?. Supn que el personaje puede quitar el silenciador. Qu cambios habra que hacer en el cdigo para quitar el decorador a la instancia?
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 exible 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: rie, rie con silenciador, rie con cargador extra y rie con silenciador y cargador. Sin duda, este
esquema es muy poco exible.
4.3.3.
Facade
C4
[122]
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 unicada 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.6: Simplicacin utilizando Facade
1 AnimationManager* animation = new AnimationManager();
2 animation->explosion_at(3,4);
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 denitiva, el uso del patrn Facade proporciona una estructura de diseo como
la mostrada en la gura 4.8.
Consideraciones
El patrn Facade puede ser til cuando:
[123]
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.
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 exibilidad 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 exibles 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 denitiva, no proporcionar un nivel de
abstraccin adecuado.
4.3.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).
Problema
Programas como los videojuegos requieren la interaccin de un usuario que, normalmente, realiza diferentes acciones sobre una interfaz grca. 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 grca.
Solucin
En el patrn MVC, mostrado en la gura 4.9, existen tres entidades bien denidas:
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.
C4
[124]
Consideraciones
El patrn MVC es la losofa 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 exibilidad a la
hora de crear diferentes tipos de presentaciones para un mismo dominio.
De hecho, desde un punto de vista general, la estructura ms utilizada en los videojuegos se asemeja a un patrn MVC: la interfaz grca utilizando grcos 3D/2D
(vista), bucle de eventos (controlador) y las estructuras de datos internas (modelo).
4.3.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 modicar 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 modicacin 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 modicada.
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.
[125]
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.
4.3.6.
Proxy
C4
Solucin
[126]
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 gura 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.
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.7: Ejemplo de implementacin de Proxy
1 class Graphic {
2 public:
3
void display() = 0;
4 };
5
[127]
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
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.4.
Patrones de comportamiento
C4
[128]
4.4.1.
Observer
[129]
C4
[130]
4.4.2.
State
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 denitiva, 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.
[131]
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 noticar 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.4.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.
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 habitual 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 gura 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.
C4
Consideraciones
[132]
Implementacin
A continuacin, se muestra una implementacin simplicada y aplicada a una estructura de datos de tipo lista. Ntese cmo utilizando las primitivas que ofrece la
estructura, el iterator proporciona una visin de acceso secuencial a travs del mtodo
next().
Listado 4.8: Iterator (ejemplo)
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
[133]
35
36
37
38
39
40
41
42
43
44
45
46
47
bool isDone() {
return _currentIndex > _list->length();
};
};
C4
Consideraciones
La ventaja fundamental de utilizar el patrn Iterator es la simplicacin 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:
Aadir un mtodo virtual en la clase padre de todas las estructuras de forma
que los clientes puedan crear el nuevo tipo iterador. Esto puede tener un gran
impacto porque puede haber estructuras que no se pueda o no se desee acceder
con el nuevo iterador.
Aadir un nuevo tipo de estructura que sea dependiente del nuevo tipo del iterador. Por ejemplo, RandomizedList que devolvera un iterador RandomIterator
y que accede de forma aleatoria a todos los elementos.
4.4.4.
Template Method
[134]
Por ejemplo, supongamos que tenemos dos tipos de jugadores de juegos de mesa:
ajedrez y damas. En esencia, ambos juegan igual; lo que cambia son las reglas del
juego que, obviamente, condiciona su estrategia y su forma de jugar concreta. Sin
embargo, en ambos juegos, los jugadores mueven en su turno, esperan al rival y esto
se repite hasta que acaba la partida.
El patrn Template Method consiste extraer este comportamiento comn en una
clase padre y denir en las clases hijas la funcionalidad concreta.
Solucin
Siguiendo con el ejemplo de los jugadores de ajedrez y damas, la gura 4.16 muestra una posible aplicacin del patrn Template Method a modo de ejemplo. Ntese que
la clase GamePlayer es la que implementa el mtodo play() que es el que invoca
a los otros mtodos que son implementados por las clases hijas. Este mtodo es el
mtodo plantilla.
Cada tipo de jugador dene los mtodos en base a las reglas y heursticas de su
juego. Por ejemplo, el mtodo isOver() indica si el jugador ya no puede seguir
jugando porque se ha terminado el juego. En caso de las damas, el juego se acaba para
el jugador si se ha quedado sin chas; mientras que en el caso ajedrez puede ocurrir
por jaque mate (adems de otros motivos).
Consideraciones
Algunas consideraciones sobre el patrn Template Method:
Utilizando este patrn se suele obtener estructuras altamente reutilizables.
Introduce el concepto de operaciones hook que, en caso de no estar implementadas en las clases hijas, tienen una implementacin por defecto. Las clases hijas
pueden sobreescribirlas para aadir su propia funcionalidad.
[135]
Strategy
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 congurarse 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.9: Uso de los jugadores (Strategy)
1
2
3
4
5
C4
4.4.5.
[136]
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 benecios:
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.4.6.
Reactor
[137]
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 reere 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 chero.
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 desmultiplexacin 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().
En general, el comportamiento sera el siguiente:
1. Los manejadores se registran utilizando el mtodo regHandler() del Reactor. De esta forma, el Reactor puede congurarse para esperar los eventos del
recurso que el manejador espera. El manejador puede dejar de recibir noticaciones con unregHandler().
2. A continuacin, el Reactor entra en el bucle innito (loop()), en el que se
espera la ocurrencia de eventos.
3. Utilizando alguna llamada al sistema, como puede ser select(), el Reactor
espera a que se produzca algn evento sobre los recursos monitorizados.
4. Cuando ocurre, busca los manejadores asociados a ese recurso y les invoca el
mtodo handle() con el evento que ha ocurrido como parmetro.
5. El manejador recibe la invocacin y ejecuta todo el cdigo asociado al evento.
Ntese que aunque los eventos ocurran concurrentemente el Reactor serializa las
llamadas a los manejadores. Por lo tanto, la ejecucin de los manejadores de eventos
ocurre de forma secuencial.
Consideraciones
Al utilizar un Reactor, se deben tener las siguientes consideraciones:
1. Los manejadores de eventos no pueden consumir mucho tiempo. Si lo hacen,
pueden provocar un efecto convoy y, dependiendo de la frecuencia de los eventos, pueden hacer que el sistema sea inoperable. En general, cuanto mayor sea
la frecuencia en que ocurren los eventos, menos tiempo deben consumir los
manejadores.
2. Existen implementaciones de Reactors que permiten una desmultiplexacin concurrente.
3. Desde un punto de vista general, el patrn Observer tiene un comportamiento
muy parecido. Sin embargo, el Reactor est pensado para las relaciones 1 a 1 y
no 1 a n como en el caso del Observer.
C4
[138]
4.4.7.
Visitor
[139]
C4
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;
};
[140]
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) {};
};
/* client usage */
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);
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 inuir
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.
[141]
Hasta el momento, se han descrito algunos de los patrones de diseo ms importantes que proporcionan una buena solucin a determinados problemas a nivel de diseo. Todos ellos son aplicables a cualquier lenguaje de programacin en mayo o menor
medida: algunos patrones de diseo son ms o menos difciles de implementar en un
lenguaje de programacin determinado. Depender de las estructuras de abstraccin
que ste proporcione.
Sin embargo, a nivel de lenguaje de programacin, existen patrones que hacen
que un programador comprenda mejor dicho lenguaje y aplique soluciones mejores,
e incluso ptimas, a la hora de resolver un problema de codicacin. Los expresiones
idiomticas (o simplemente, idioms) son un conjunto de buenas soluciones de programacin que permiten:
Resolver ciertos problemas de codicacin, normalmente asociados a un lenguaje especco. Por ejemplo, cmo obtener la direccin de memoria real de
un objeto en C++ aunque est sobreescrito el operador &. Este idiom se llama
AddressOf.
Entender y explotar las interioridades del lenguaje y, por tanto, programar mejor.
Establecer un repertorio de buenas prcticas de programacin para el lenguaje.
Al igual que los patrones, los idioms tienen un nombre asociado (adems de sus
alias), la denicin del problema que resuelven y bajo qu contexto, as como algunas
consideraciones (eciencia, etc.). A continuacin, se muestran algunos de los ms
relevantes. En la seccin de Patrones de Diseo Avanzados se exploran ms de
ellos.
4.5.1.
#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;
}
C4
Programming Idioms
4.5.
[142]
4.5.2.
Interface Class
[143]
class Vehicle
{
public:
virtual ~Vehicle();
virtual std::string name() = 0;
virtual void run() = 0;
};
class Car: public Vehicle
{
public:
virtual ~Car();
std::string name() { return "Car"; }
void run() { /* ... */ }
};
class Motorbike: public Vehicle
{
public:
virtual ~Motorbike();
std::string name() { return "Motorbike"; }
void run() { /* ... */ }
};
Este idiom muestra cmo crear una clase que se comporta como una interfaz,
utilizando mtodosvirtuales puros.
El compilador fuerza que los mtodos virtuales puros sean implementados por
las clases derivadas, por lo que fallar en tiempo de compilacin si hay alguna que
no lo hace. Como resultado, tenemos que Vehicle acta como una clase interfaz.
Ntese que el destructor se ha declarado como virtual. Como ya se cit en la forma
cannica ortodoxa (seccin 4.5.1), esto es una buena prctica para evitar posibles leaks
de memoria en tiempo de destruccin de un objeto Vehicle usado polimrcamente
(por ejemplo, en un contenedor). Con esto, se consigue que se llame al destructor de
la clase ms derivada.
4.5.3.
Final Class
En Java o C# es posible denir una clase como final, es decir, no es posible heredar una clase hija de ella. Este mecanismo de proteccin puede tener mucho sentido
en diferentes ocasiones:
Desarrollo de una librera o una API que va ser utilizada por terceros.
Clases externas de mdulos internos de un programa de tamao medio o grande.
En C++, no existe este mecanismo. Sin embargo, es posible simularlo utilizando
el idiom Final Class.
Este idiom se basa en una regla de la herencia virtual: el constructor y destructor de una clase heredada virtualmente ser invocado por la clase ms derivada de
la jerarqua. Como, en este caso, el destructor es privado, el compilador prohbe la
instanciacin de B y el efecto es que no se puede heredar ms all de A.
C4
[144]
class Final
{
~Final() {} // privado
friend class A;
};
class A : virtual Final
{ };
class B : public B
{ };
int main (void)
{
B b; // fallo de compilacin
}
4.5.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 recompilar a todos sus usuarios. El objetivo es minimizar el impacto de un
cambio en la declaracin de la clase a sus usuarios.
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 denida
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.14: 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 modicarla 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.15: Clase Vehicle usando pImpl (Vehicle.h)
1
2
3
4
5
[145]
6
void run(int distance);
7
8 private:
9
class VehicleImpl;
10
VehicleImpl* _pimpl;
11 };
/* Vehicle.cpp */
#include <Vehicle.h>
Vehicle::Vehicle()
{
_pimpl = new VehicleImpl();
}
void Vehicle::run()
{
_pimpl->run();
}
/* VehicleImpl.h */
class VehicleImpl
{
public:
void run();
private:
int _wheels;
std::string name;
};
C4
Captulo
La Biblioteca STL
David Vallejo Fernndez
capitalEn este captulo se proporciona una visin general de STL (Standard Template Library), la biblioteca estndar proporcionada por C++, en el contexto del desarrollo de videojuegos, discutiendo su utilizacin en dicho mbito de programacin.
Asimismo, tambin se realiza un recorrido exhaustivo por los principales tipos de
contenedores, estudiando aspectos relevantes de su implementacin, rendimiento y
uso en memoria. El objetivo principal que se pretende alcanzar es que el lector sea capaz de utilizar la estructura de datos ms adecuada para solucionar un problema,
justicando el porqu y el impacto que tiene dicha decisin sobre el proyecto en su
conjunto.
5.1.
Usando STL
STL es sin duda una de las bibliotecas ms utilizadas en el desarrollo
de aplicaciones de C++. Adems,
est muy optimizada para el manejo de estructuras de datos y de algoritmos bsicos, aunque su complejidad es elevada y el cdigo fuente
es poco legible para desarrolladores
poco experimentados.
147
[148]
<type>
it = begin ()
<type>
++it
<type>
...
<type>
end ()
Inclusin de contenedores para almacenar datos en distintas estructuras, iteradores para acceder a los elementos de los contenedores y algoritmos para llevar
a cabo operaciones sobre los mismos.
Soporte como base comn para otras bibliotecas.
La gura 5.2 muestra una perspectiva global de la organizacin de STL y los diferentes elementos que la denen. Como se puede apreciar, STL proporciona una gran
variedad de elementos que se pueden utilizar como herramientas para la resolucin de
problemas dependientes de un dominio en particular.
Las utilidades de la biblioteca estndar se denen en el espacio de nombres std y
se encuentran a su vez en una serie de bibliotecas, que identican las partes fundamentales de STL. Note que no se permite la modicacin de la biblioteca estndar y no es
aceptable modicar su contenido mediante macros, ya que afectara a la portabilidad
del cdigo desarrollado.
En el mbito del desarrollo de videojuegos, los contenedores juegan un papel fundamental como herramienta para el almacenamiento de informacin en memoria. En
este contexto, la realizacin un estudio de los mismos, en trminos de operaciones,
gestin de memoria y rendimiento, es especialmente importante para utilizarlos adecuadamente. Los contenedores estn representados por dos tipos principales. Por una
parte, las secuencias permiten almacenar elementos en un determinado orden. Por otra
parte, los contenedores asociativos no tienen vinculado ningn tipo de restriccin de
orden.
La herramienta para recorrer los contenedores est representada por el iterador.
Todos los contenedores mantienen dos funciones relevantes que permiten obtener dos
iteradores:
1. begin(), que devuelve un iterador al primer elemento del contenedor,
2. end(), que devuelve un iterador al elemento siguiente al ltimo.
El hecho de que end() devuelva un iterador al siguiente elemento al ltimo albergado en el contenedor es una convencin en STL que simplica la iteracin sobre los
elementos del mismo o la implementacin de algoritmos.
Una vez que se obtiene un iterador que apunta a una parte del contenedor, es posible utilizarlo como referencia para acceder a los elementos contiguos. En funcin del
tipo de iterador y de la funcionalidad que implemente, ser posible acceder solamente
al elemento contiguo, al contiguo y al anterior o a uno aleatorio.
Abstraccin STL
El uso de los iteradores en STL representa un mecanismo de abstraccin fundamental para realizar el recorrido, el acceso y la modicacin
sobre los distintos elementos almacenados en un contenedor.
[149]
Iteradores
Iteradores y soporte a iteradores
<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 [94]. El asterisco referencia a elementos con el estilo
del lenguaje C.
C5
<iterator>
Algoritmos
[150]
plantillas para manejar los distintos contenedores, permitiendo separar la funcionalidad de los mismos respecto a los datos que contienen. En el ejemplo, se instancia un
vector de tipos enteros.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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;
Note cmo el incremento del iterador es trivial
(lnea 17 ), al igual que el acceso
del contenido al que apunta el iterador (lnea 18 ), mediante una nomenclatura similar
a la utilizada para manejar punteros.
Por otra parte, STL proporciona un conjunto estndar de algoritmos que se pueden
aplicar a los contenedores e iteradores. Aunque dichos algoritmos son bsicos, stos se
pueden combinar para obtener el resultado deseado por el propio programador. Algunos ejemplos de algoritmos son la bsqueda de elementos, la copia, la ordenacin, etc.
Al igual que ocurre con todo el cdigo de STL, los algoritmos estn muy optimizados
y suelen sacricar la simplicidad para obtener mejores resultados que proporcionen
una mayor eciencia.
Manejo de iteradores
El concepto de iterador es fundamental para recorrer e interactuar
de manera eciente con las distintas estructuras de datos incluidas en
la biblioteca estndar.
5.2.1.
Reutilizacin de cdigo
Uno de los principales argumentos para utilizar STL en el desarrollo de videojuegos es la reutilizacin de cdigo que ya ha sido implementado, depurado y portado a
distintas plataformas. En este contexto, STL proporciona directamente estructuras de
datos y algoritmos que se pueden utilizar y aplicar, respectivamente, para implementar
soluciones dependientes de un dominio, como por ejemplo los videojuegos.
Gran parte del cdigo de STL est vinculado a la construccin de bloques bsicos
en el desarrollo de videojuegos, como las listas, las tablas hash, y a algoritmos fundamentales como la ordenacin o la bsqueda de elementos. Adems, el diseo de STL
est basado en el uso extensivo de plantillas. Por este motivo, es posible utilizarlo
para manejar cualquier tipo de estructura de datos sin tener que modicar el diseo
interno de la propia aplicacin.
C5
5.2.
[151]
[152]
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 grca 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.
STL ser normalmente ms eciente que otra implementacin y, por lo tanto, es
el candidato ideal para manejar las estructuras de datos de un videojuego. Mejorar el
rendimiento de STL implica tener un conocimiento muy profundo de las estructuras
de datos a manejar y puede ser una alternativa en casos extremos y con plataformas
hardware especcas. Si ste no es el caso, entonces STL es la alternativa directa.
No obstante, algunas compaas tan relevantes en el mbito del desarrollo de videojuegos, como EA (Electronic Arts), han liberado su propia adaptacin de STL denominada EASTL (Electronic Arts Standard Template Library) 1 , justicando esta
decisin en base a la deteccin de ciertas debilidades, como el modelo de asignacin
de memoria, o el hecho de garantizar la consistencia desde el punto de vista de la
portabilidad.
Adems del rendimiento de STL, es importante considerar que su uso permite
que el desarrollador se centre principalmente en el manejo de elementos propios, es
decir, a nivel de nodos en una lista o claves en un diccionario, en lugar de prestar ms
importancia a elementos de ms bajo nivel, como punteros o buffers de memoria.
Uno de los aspectos claves a la hora de manejar de manera eciente STL se basa en
utilizar la herramienta adecuada para un problema concreto. Si no es as, entonces es
fcil reducir enormemente el rendimiento de la aplicacin debido a que no se utiliz,
por ejemplo, la estructura de datos ms adecuada.
5.2.3.
Inconvenientes
es lgico suponer que dicho entorno tenga suciente memoria y la penalizacin por
asignar memoria no es crtica. Aunque esta situacin es la ms comn a la hora de
1 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2271.html
La herramienta ideal
Benjamin Franklin arm que si
dispusiera de 8 horas para derribar
un rbol, empleara 6 horas para alar su hacha. Esta reexin se puede
aplicar perfectamente a la hora de
decidir qu estructura de datos utilizar para solventar un problema.
[153]
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.
Una posible solucin a este inconveniente consiste en utilizar asignadores de memoria personalizados cuando se utilizan contenedores especcos, 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.
Cabecera
#elementos
Tamao elemento
Inicio
Elemento 1
Elemento 2
Elemento 3
...
Elemento i
...
Elemento n
Vaco
Figura 5.5: Implementacin tpica
del contenedor vector con cabecera.
5.3.
Secuencias
5.3.1.
Vector
C5
5.3. Secuencias
[154]
2 http://www.cplusplus.com/reference/stl/vector/
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).
[155]
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 [].
Vectores y reserve()
La reserva de memoria explcita
puede contribuir a mejorar el rendimiento de los vectores y evitar un
alto nmero de operaciones de reserva.
#include <iostream>
#include <vector>
using namespace std;
int main () {
vector<int> v;
v.reserve(4);
cout << "Capacidad inicial: " << v.capacity() << endl;
v.push_back(7); v.push_back(6);
v.push_back(4); v.push_back(6);
cout << "Capacidad actual: " << v.capacity() << endl;
v.push_back(4); // Provoca pasar de 4 a 8.
// Se puede evitar con v.reserve (8) al principio.
cout << "Capacidad actual: " << v.capacity() << endl;
return 0;
}
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.
Finalmente, es posible aprovecharse del hecho de que los elementos de un vector
se almacenan de manera contigua en memoria. Por ejemplo, sera posible pasar el
contenido de un vector a una funcin que acepte un array, siempre y cuando dicha
funcin no modique su contenido [27], ya que el contenido del vector no estara
sincronizado.
C5
5.3. Secuencias
[156]
5.3.2.
Deque
Deque proviene del trmino ingls double-ended queue y representa una cola de
doble n. 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 n incluyen la funcionalidad bsica de los vectores pero tambin considera operaciones para insertar y eliminar elementos, de manera explcita, al
principio del contenedor3 .
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.
La cabecera de la cola de doble n 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.
3 http://www.cplusplus.com/reference/stl/deque/
Deque y punteros
La cola de doble n 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.
5.3. Secuencias
[157]
Bloque 1
Principio
Final
#elementos
Tamao elemento
Vaco
C5
Cabecera
Elemento 1
Elemento 2
Bloque 1
Bloque 2
Bloque 2
Elemento 3
Bloque n
Elemento 4
Elemento 5
Vaco
Bloque n
Elemento 6
Elemento j
Elemento k
Vaco
Rendimiento
Al igual que ocurre con los vectores, la insercin y eliminacin de elementos al
nal 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 nal. La consecuencia directa es que las colas de doble n son el candidato
perfecto para estructuras FIFO (First In, First Out).
FIFO
Las estructuras First In, First Out
se suelen utilizar en el mbito del
desarrollo de videojuegos 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 n es prcticamente igual de rpido que en el caso de un vector.
Respecto al uso de memoria, es importante resaltar que las asignaciones se producen de manera peridica durante el uso normal de la cola de doble n. Por una parte,
si se aaden nuevos elementos, entonces se reservan nuevos bloques memoria. Por
otra parte, si se eliminan elementos, entonces se liberan otros bloques. Sin embargo,
y aunque el nmero de elementos permanezca constante, es posible que se sigan asignando nuevos bloques si, por ejemplo, los elementos se aaden al nal y se eliminan
al principio. Desde un punto de vista abstracto, este contenedor se puede interpretar
como una ventana deslizante en memoria, aunque siempre tenga el mismo tamao.
Finalmente, y debido a la naturaleza dinmica de la cola de doble n, no existe
una funcin equivalente a la funcin reserve de vector.
[158]
5.3.3.
List
La lista es otro contenedor de secuencia que diere 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 n. 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 especicacin de listas de STL ofrece una funcionalidad bsica muy similar
a la de cola de doble n, pero tambin incluye funcionalidad relativa a aspectos ms
complejos como la fusin de listas o la bsqueda de elementos4 .
Implementacin
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.
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
gura 5.7).
La lista tambin tiene vinculada una cabecera con informacin relevante, destacando especialmente los punteros al primer y ltimo elemento de la lista.
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.
Una desventaja con respecto a los vectores y a las colas de doble n 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.
4 http://www.cplusplus.com/reference/stl/list/
Entre listas...
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.
5.3. Secuencias
[159]
Principio
Siguiente
Siguiente
Siguiente
Final
Anterior
Anterior
Anterior
Elemento 1
Elemento 2
Elemento 3
#elementos
Tamao elemento
...
#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; }
// Sobrecarga del operador para poder comparar clases.
bool operator< (const Clase &c) const {
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;
C5
Cabecera
[160]
El planteamiento basado en manejar punteros hace que las listas sean ms ecientes a la hora de, por ejemplo, reordenar sus elementos, ya que no es necesario realizar
copias de datos. En su lugar, simplemente hay que modicar 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 benecio 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
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.
[161]
Propiedad
I/E al nal
I/E al principio
I/E en el medio
Recorrido
Asignacin memoria
Acceso memoria sec.
Invalidacin iterador
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
Cuadro 5.1: Resumen de las principales propiedades de los contenedores de secuencia previamente estudiados (I/E = insercin/eliminacin).
5.4.
Contenedores asociativos
5.4.1.
Set y multiset
C5
[162]
#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;
Claves en sets
El contenedor set tiene como caracterstica principal que los elementos almacenados en el mismo actan como las propias claves.
[163]
Cabecera
C5
Principio
Final
Hijoi
#elementos
Elemento
Hijod
Tamao elemento
...
Hijoi
Hijoi
Elemento
Elemento
Hijodi
Hijodi
Hijoi
Hijodi
Elemento
Elemento
Hijod
Hijodi
Implementacin
En trminos generales, los conjuntos se implementan utilizando un rbol binario
balanceado (ver gura 5.9) para garantizar que la complejidad de las bsquedas en
el peor caso posible sea logartmica, es decir, O(logn). Cada elemento, al igual que
ocurre con la lista, est vinculado a un nodo que enlaza con otros manteniendo una
estructura de rbol. Dicho rbol se ordena con la funcin de comparacin especicada
en la plantilla. Aunque este enfoque es el ms comn, es posible encontrar alguna
variacin en funcin de la implementacin de STL utilizada.
Rendimiento
La principal baza de los conjuntos es su gran rendimiento en operaciones relativas
a la bsqueda de elementos, garantizando una complejidad logartmica en el peor
de los casos. Sin embargo, es importante tener en cuenta el tiempo invertido en la
comparacin de elementos, ya que sta no es trivial en caso de manejar estructuras de
datos ms complejas, como por ejemplo las matrices. As mismo, la potencia de los
conjuntos se demuestra especialmente cuando el nmero de elementos del contenedor
comienza a crecer.
Comparando elementos
Aunque los conjuntos permiten realizar bsquedas ecientes, es importante tener en cuenta la complejidad asociada a la propia comparacin de elementos, especialmente si
se utilizan estructuras de datos complejas.
Una alternativa posible para buscar elementos de manera eciente puede ser un
vector, planteando para ello una ordenacin de elementos seguida de una bsqueda
binaria. Este planteamiento puede resultar eciente 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.
[164]
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
especcas, 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.
5.4.2.
Map y multimap
En STL, el contenedor map se puede interpretar como una extensin de los conjuntos con el principal objetivo de optimizar an ms la bsqueda de elementos. Para
ello, este contenedor distingue entre clave y valor para cada elemento almacenado en
el mismo. Este planteamiento diere de los conjuntos, ya que estos hacen uso del propio elemento como clave para ordenar y buscar. En esencia, un map es una secuencia
de pares <clave, valor>, proporcionando una recuperacin rpida del valor a partir de
dicha clave. As mismo, un map se puede entender como un tipo especial de array
que permite la indexacin de elementos mediante cualquier tipo de datos. De hecho,
el contenedor permite el uso del operador [].
El multimap permite manejar distintas instancias de la misma clave, mientras que
el map no. Es importante resaltar que no existe ninguna restriccin respecto al valor,
es decir, es posible tener un mismo objeto como valor asociado a distintas claves.
La principal aplicacin de un map es manejar estructuras de tipo diccionario que
posibiliten el acceso inmediato a un determinado valor dada una clave. Por ejemplo,
sera posible asociar un identicador nico a cada una de las entidades de un juego
y recuperar los punteros asociados a las propias entidades cuando as sea necesario.
De este modo, es posible utilizar slo la memoria necesaria cuando se aaden nuevas
entidades.
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.
Clave
Mapping
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.
7 http://www.cplusplus.com/reference/stl/map/
8 http://www.cplusplus.com/reference/stl/multimap/
Valor
Figura 5.10: El proceso de utilizar
una clave en un map para obtener
un valor se suele denominar comnmente mapping.
[165]
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 denidos por el usuario, el rendimiento puede verse afectado negativamente.
El operador []
El operador [] se puede utilizar para acceder, escribir o actualizar informacin sobre algunos contenedores. Sin embargo, es importante
considerar el impacto en el rendimiento al utilizar dicho operador, el
cual vendr determinado por el contenedor usado.
Por otra parte, el uso del operador [] no tiene el mismo rendimiento que en vectores, ya que implica realizar la bsqueda del elemento a partir de la clave y, por lo tanto,
no sera de un orden de complejidad constante sino logartmico. Al usar este operador,
tambin hay que tener en cuenta que si se escribe sobre un elemento que no existe,
entonces 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
En la lnea 7 se hace uso de pair paradeclarar
una pareja de valores: i) un itera
dor al contenedor denido en la lnea 6 y ii) un valor booleano. Esta
estructura
se utilizar para recoger el valor de retorno de una insercin (lnea 14 ).
#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;
}
C5
Rendimiento
[166]
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
Cuadro 5.2: Resumen de las principales propiedades de los contenedores asociativos previamente estudiados (I/E = insercin/eliminacin).
22
23
24
25
26
27
28 }
Ante esta situacin, resulta deseable hacer uso de la operacin nd 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 eciente
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
eciente 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.
Cundo usar maps y multimaps?
En general, estos contenedores se utilizan como diccionarios de rpido acceso
que permiten indexar elementos a partir de manejadores o identicadores nicos. Sin
embargo, tambin es posible utilizar claves ms complejas, como por ejemplo cadenas. Algunas de las principales aplicaciones en el desarrollo de videojuegos son las
siguientes:
Mantenimiento de un diccionario con identicadores nicos para mapear las
distintas entidades del juego. De este modo, es posible evitar el uso de punteros
a dichas entidades.
Mecanismo de traduccin entre elementos del juego, como por ejemplo entre
identicadores numricos y cadenas de texto vinculadas a los nombres de los
personajes.
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 eciente.
[167]
Adaptadores de secuencia
STL proporciona el concepto de adaptadores para proporcionar una funcionalidad ms restringida sobre un contenedor existente sin la necesidad de que el propio
desarrollador tenga que especicarla. Este enfoque se basa en que, por ejemplo, los
contenederos de secuencia existentes tienen la exibilidad necesaria para comportarse como otras estructuras de datos bien conocidas, como por ejemplo las pilas o las
colas.
LIFO
La pila est disea para operar en un
contexto LIFO (Last In, First Out)
(last-in rst-out), es decir, el elemento que se apil ms recientemente ser el primero en salir de la
pila.
Un pila es una estructura de datos que solamente permite aadir y eliminar elementos por un extremo, la cima. En este contexto, cualquier conteneder de secuencia
discutido anteriormente se puede utilizar para proporcionar dicha funcionalidad. En
el caso particular de STL, es posible manejar pilas e incluso especicar la implementacin subyacente que se utilizar, especicando de manera explcita cul ser el
contenedor de secuencia usado.
Aunque sera perfectamente posible delegar en el programador el manejo del contenedor de secuencia para que se comporte como, por ejemplo, una pila, en la prctica
existen dos razones importantes para la denicin de adaptadores:
1. La declaracin explcita de un adaptador, como una pila o stack, hace que sea el
cdigo sea mucho ms claro y legible en trminos de funcionalidad. Por ejemplo, declarar una pila proporciona ms informacin que declarar un vector que
se comporte como una pila. Esta aproximacin facilita la interoperabilidad con
otros programadores.
Cima
Elemento 1
Elemento 2
Elemento 3
5.5.1.
Stack
Elemento i
...
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.
...
Elemento n
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int main () {
vector<int> fichas;
9 http://www.cplusplus.com/reference/stl/stack/
C5
5.5.
[168]
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 }
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
nal 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.
Por defecto, la cola utiliza la implementacin de la cola de doble n, siendo posible
utilizar la implementacin de la lista. Sin embargo, no es posible utilizar el vector
debido a que este contenedor no proporciona la operacin push_front, requerida por
el propio adaptador. De cualquier modo, el rendimiento de un vector para eliminar
elementos al principio no es particularmente bueno, ya que tiene una complejidad
lineal.
5.5.3.
Cola de prioridad
caso especial de cola previamente discutido. La diferencia entre los dos adaptadores
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 especca, a diferencia del resto de adaptadores 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 debido a que la cola con prioridad requiere un acceso aleatorio para insertar ecientemente
elementos ordenados.
La comparacin de elementos sigue el mismo esquema que el estudiado en la seccin 5.4.1 y que permita denir el criterio de inclusin de elementos en un conjunto,
el cual estaba basado en el uso del operador menor que.
10 http://www.cplusplus.com/reference/stl/queue/
11 http://www.cplusplus.com/reference/stl/priority_queue/
Insercin
Elemento 1
Elemento 2
Elemento 3
...
Elemento i
...
Elemento n
Extraccin
Figura 5.12: Visin abstracta de
una cola o queue. Los elementos solamente se aaden por el nal y se
eliminan por el principio.
[169]
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.
C5
Captulo
Gestin de Recursos
David Vallejo Fernndez
171
[172]
6.1.
El bucle de juego
6.1.1.
El bucle de renderizado
Figura 6.1: El bucle de juego representa la estructura de control principal de cualquier juego y gobierna su
funcionamiento y la transicin entre
los distintos estados del mismo.
Rectangle invalidation
La tcnica basada en redibujar nicamente aquellas partes de la pantalla cuyo contenido cambia realmente se suele denominar rectangle invalidation.
6.1.2.
Como ya se introdujo en la seccin 1.2, en un motor de juegos existe una gran variedad de subsistemas o componentes con distintas necesidades. Algunos de los ms
importantes son el motor de renderizado, el sistema de deteccin y gestin de colisiones, el subsistema de juego o el subsistema de soporte a la Inteligencia Articial.
[173]
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 sucientemente 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 Articial.
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 // Pseudocdigo de un juego tipo "Pong".
2 int main (int argc, char* argv[]) {
3
init_game();
// Inicializacin del juego.
4
5
// Bucle del juego.
6
while (1) {
7
capture_events();
// Capturar eventos externos.
8
9
if (exitKeyPressed()) // Salida.
10
break;
11
12
move_paddles();
// Actualizar palas.
13
move_ball();
// Actualizar bola.
14
collision_detection(); // Tratamiento de colisiones.
15
16
// Anot algn jugador?
17
if (ballReachedBorder(LEFT_PLAYER)) {
18
score(RIGHT_PLAYER);
19
reset_ball();
20
}
21
if (ballReachedBorder(RIGHT_PLAYER)) {
22
score(LEFT_PLAYER);
23
reset_ball();
24
}
25
26
render();
// Renderizado.
27
}
28 }
C6
[174]
6.1.3.
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.
Tratamiento de mensajes en Windows
En las plataformas WindowsTM , los juegos han de atender los mensajes recibidos
por el propio sistema operativo y dar soporte a los distintos componentes del propio
motor de juego. Tpicamente, en estas plataformas se implementan los denominados
message pumps [42], como responsables del tratamiento de este tipo de mensajes.
La principal consecuencia de este enfoque es que los mensajes del sistema operativo tienen prioridad con respecto a aspectos crticos como el bucle de renderizado.
Por ejemplo, si la propia ventana en la que se est ejecutando el juego se arrastra o su
tamao cambia, entonces el juego se congelar a la espera de nalizar el tratamiento
de eventos recibidos por el propio sistema operativo.
Esquemas basados en retrollamadas
message dispatching
El concepto de retrollamada o callback, introducido de manera implcita en la discusin del patrn MVC de la seccin 4.3.4, consiste en asociar una porcin de cdigo
para atender un determinado evento o situacin. Este concepto se puede asociar a una
funcin en particular o a un objeto. En este ltimo caso, dicho objeto se denominar
callback object, trmino muy usado en el desarrollo de interfaces grcas de usuario
y en el rea de los sistemas distribuidos.
A continuacin se muestra un ejemplo de uso de funciones de retrollamada planteado en la biblioteca GLUT, la cual est estrechamente ligada a la biblioteca GL,
utilizada para tratar de manera simple eventos bsicos.
Listado 6.3: Ejemplo de uso de retrollamadas con OpenGL.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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);
Sistema
operativo
Figura 6.3: Esquema grco de
una arquitectura basada en message
pumps.
[175]
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;
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.
C6
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 }
[176]
Intro
Men
ppal
Juego
Game
over
Pausa
Figura 6.5: Visin general de una mquina de estados nita que representa los estados ms comunes en
cualquier juego.
Finite-state machines
Las mquinas de estados o autmatas representan modelos matemticos utilizados para disear programas y lgica digital. En el caso del
desarrollo de videojuegos se pueden usar para modelar diagramas de
estados para, por ejemplo, denir
los distintos comportamientos de un
personaje.
OIS::
MouseListener
Ogre::
FrameListener
OIS::
KeyListener
OIS::
MouseListener
Ogre::
Singleton
C6
Ogre::
Singleton
[177]
InputManager
1
OIS::
Keyboard
GameManager
1..*
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 reejan las clases especcas de dominio.
6.1.4.
[178]
El siguiente listado de cdigo muestra el esqueleto de la clase GameState implementada en C++. Como se puede apreciar, esta clase es abstracta ya que mantiene
una serie de funciones miembro como virtuales puras, con el objetivo de forzar su
implementacin en las clases que deriven de ella y que, por lo tanto, denan estados especcos de un juego. Estas funciones virtuales puras se pueden dividir en tres
grandes bloques:
1. Gestin bsica del estado (lneas 19-22 ), para denir qu hacer cuando se
entra, sale, pausa o reanuda el estado.
#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
[179]
36
// de eventos antes y despus de renderizar un frame.
37
virtual bool frameStarted (const Ogre::FrameEvent& evt) = 0;
38
virtual bool frameEnded (const Ogre::FrameEvent& evt) = 0;
39
40
// Gestin bsica de transiciones.
41
void changeState (GameState* state) {
42
GameManager::getSingletonPtr()->changeState(state);
43
}
44
void pushState (GameState* state) {
45
GameManager::getSingletonPtr()->pushState(state);
46
}
47
void popState () {
48
GameManager::getSingletonPtr()->popState();
49
}
50
51 };
52
53 #endif
Sin embargo, antes de pasar a discutir esta clase, en el diseo discutido se contempla la denicin explcita de la clase InputManager, como punto central para la
gestin de eventos de entrada, como por ejemplo los de teclado o de ratn.
Ogre::Singleton?
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 aquellas 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.
Listado 6.5: Clase InputManager.
1 // SE OMITE PARTE DEL CDIGO FUENTE.
2 // Gestor para los eventos de entrada (teclado y ratn).
3 class InputManager : public Ogre::Singleton<InputManager>,
4
public OIS::KeyListener, public OIS::MouseListener {
5
public:
6
InputManager ();
7
virtual ~InputManager ();
8
9
void initialise (Ogre::RenderWindow *renderWindow);
10
void capture ();
11
12
// Gestin de listeners.
13
void addKeyListener (OIS::KeyListener *keyListener,
14
const std::string& instanceName);
15
void addMouseListener (OIS::MouseListener *mouseListener,
16
const std::string& instanceName );
17
void removeKeyListener (const std::string& instanceName);
18
void removeMouseListener (const std::string& instanceName);
19
void removeKeyListener (OIS::KeyListener *keyListener);
20
void removeMouseListener (OIS::MouseListener *mouseListener);
21
22
OIS::Keyboard* getKeyboard ();
23
OIS::Mouse* getMouse ();
24
25
// Heredados de Ogre::Singleton.
26
static InputManager& getSingleton ();
27
static InputManager* getSingletonPtr ();
28
29
private:
30
// Tratamiento de eventos.
31
// Delegar en los listeners.
32
bool keyPressed (const OIS::KeyEvent &e);
33
bool keyReleased (const OIS::KeyEvent &e);
34
35
bool mouseMoved (const OIS::MouseEvent &e);
C6
[180]
36
bool mousePressed (const OIS::MouseEvent &e,
37
OIS::MouseButtonID id);
38
bool mouseReleased (const OIS::MouseEvent &e,
39
OIS::MouseButtonID id);
40
41
OIS::InputManager *_inputSystem;
42
OIS::Keyboard *_keyboard;
43
OIS::Mouse *_mouse;
44
std::map<std::string, OIS::KeyListener*> _keyListeners;
45
std::map<std::string, OIS::MouseListener*> _mouseListeners;
46 };
Singleton<GameManager>,
5
public OIS::KeyListener, public OIS::MouseListener
6 {
7
public:
8
GameManager ();
9
~GameManager (); // Limpieza de todos los estados.
10
11
// Para el estado inicial.
12
void start (GameState* state);
13
14
// Funcionalidad para transiciones de estados.
15
void changeState (GameState* state);
16
void pushState (GameState* state);
17
void popState ();
18
19
// Heredados de Ogre::Singleton...
20
21
protected:
22
Ogre::Root* _root;
23
Ogre::SceneManager* _sceneManager;
24
Ogre::RenderWindow* _renderWindow;
25
26
// Funciones de configuracin.
27
void loadResources ();
28
bool configure ();
29
30
// Heredados de FrameListener.
[181]
bool frameStarted (const Ogre::FrameEvent& evt);
bool frameEnded (const Ogre::FrameEvent& evt);
private:
// Funciones para delegar eventos de teclado
// y ratn en el estado actual.
bool keyPressed (const OIS::KeyEvent &e);
bool keyReleased (const OIS::KeyEvent &e);
bool mouseMoved (const OIS::MouseEvent &e);
bool mousePressed (const OIS::MouseEvent &e, OIS::MouseButtonID
id);
bool mouseReleased (const OIS::MouseEvent &e, OIS::MouseButtonID
id);
42
43
44
// Gestor de eventos de entrada.
45
InputManager *_inputMgr;
46
// Estados del juego.
47
std::stack<GameState*> _states;
48 };
Note que esta clase contiene una funcin miembro start() (lnea 12 ), denida de
manera explcita para inicializar el gestor de juego, establecer el estado inicial (pasado
como parmetro) y arrancar el bucle de renderizado.
Listado 6.7: Clase GameManager. Funcin start().
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
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();
C6
31
32
33
34
35
36
37
38
39
40
41
[182]
PlayState
IntroState
tecla 'p'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PauseState
PauseState
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 siguiente listado de cdigo muestra cmo la implementacin de, por ejemplo, la
funcin keyPressed() es trivial.
Listado 6.9: Clase GameManager. Funcin keyPressed().
1
2
3
4
5
6
7
bool
GameManager::keyPressed
(const OIS::KeyEvent &e)
{
_states.top()->keyPressed(e);
return true;
}
6.1.5.
Este esquema de gestin de estados general, el cual contiene una clase genrica
GameState, permite la denicin de estados especcos vinculados a un juego en particular. En la gura 6.6 se muestra grcamente cmo la clase GameState se extiende
para denir tres estados:
IntroState, que dene un estado de introduccin o inicializacin.
PlayState, que dene el estado principal de un juego y en el que se desarrollar
la lgica del mismo.
PlayState
IntroState
[183]
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
C6
[184]
4 }
5
6 void PauseState::keyPressed (const OIS::KeyEvent &e) {
7
if (e.key == OIS::KC_P) // Tecla p ->Estado anterior.
8
popState();
9 }
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 especco 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 eciente 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.
6.2.1.
Ogre facilita la carga, liberacin y gestin de recursos mediante un planteamiento bastante sencillo centrado en optimizar el consumo de memoria de la aplicacin
grca. Recuerde que la memoria es un recurso escaso en el mbito del desarrollo de
videojuegos.
Media manager
El gestor de recursos o resource
manager se suele denominar comnmente media manager o asset
manager.
Ogre::
ResourceManager
Ogre::
SharedPtr
Ogre::
Resource
NuevoRecursoPtr
NuevoRecurso
C6
Ogre::
Singleton
[185]
NuevoGestor
0..*
Figura 6.11: Diagrama de clases de las principales clases utilizadas en Ogre3D para la gestin de recursos.
En un tono ms oscuro se reejan las clases especcas de dominio.
Carga de niveles
Una de las tcnicas bastantes comunes a la hora de cargar niveles de
juego consiste en realizarla de manera previa al acceso a dicho nivel.
Por ejemplo, se pueden utilizar estructuras de datos para representar
los puntos de acceso de un nivel al
siguiente para adelantar el proceso
de carga. El mismo planteamiento
se puede utilizar para la carga de
texturas en escenarios.
[186]
6.2.2.
Introduccin a SDL
SDL (Simple Directmedia Layer) es una biblioteca multimedia y multiplataforma
ampliamente utilizada en el mbito del desarrollo de aplicaciones multimedia. Desde
un punto de vista funcional, SDL proporciona una serie de APIs para el manejo de
vdeo, audio, eventos de entrada, multi-hilo o renderizado con OpenGL, entre otros
aspectos. Desde un punto de vista abstracto, SDL proporciona una API consistente de
manera independiente a la plataforma de desarrollo.
SDL est bien estructurado y es fcil de utilizar. La losofa de su diseo se puede resumir en ofrecer al desarrollador diversas herramientas que se pueden utilizar
de manera independiente, en lugar de manejar una biblioteca software de mayor envergadura. Por ejemplo, un juego podra hacer uso de la biblioteca SDL nicamente
para la gestin de sonido, mientras que la parte grca sera gestionada de manera
independiente (utilizando Ogre3D, por ejemplo).
SDL se puede integrar perfectamente con OpenGL para llevar a cabo la inicializacin de la parte grca de una aplicacin interactiva, delegando en SDL el propio
tratamiento de los eventos de entrada. Hasta ahora, en el mdulo 2 (Programacin
Grca) 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 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 supercies.
Event handlers
SDL posibilita la denicin de ar-
SDL, de manera que SDL oculta todos los aspectos dependientes de la plataforma a
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
Para generar un chero ejecutable, simplemente es necesario enlazar con las bibliotecas necesarias.
2 GLUT fue concebido para utilizarse en un entorno ms acadmico, simplicando el tratamiento de
eventos y la gestin de ventanas.
[187]
Listado 6.12: Ejemplo sencillo SDL + OpenGL.
#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;
Como se puede apreciar en el listado anterior, la lnea 20 establece el modo de vdeo con soporte para
para, posteriormente, hacer uso de comandos OpenGL
OpenGL
del contenido entre el
a partir de la lnea 29 . Sin embargo, note cmo el
intercambio
front buffer y el back buffer se realiza en la lnea 47 mediante una primitiva de SDL.
Listado 6.13: Ejemplo de Makele para integrar SDL.
1
2
3
4
5
6
7
CFLAGS := -c -Wall
LDFLAGS := sdl-config --cflags --libs -lSDL_image -lGL
LDLIBS := -lSDL_image -lGL
CC := gcc
all: basic_sdl_opengl
C6
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
[188]
8
9
10
11
12
13
14
15
16
17
18
19
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 dene 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 #include <SDL/SDL_mixer.h>
2 #include <OGRE/Ogre.h>
3
4 class Track : public Ogre::Resource {
5
public:
6
// Constructor (ver Ogre::Resource).
7
Track (Ogre::ResourceManager* pManager,
8
const Ogre::String& resource_name,
9
Ogre::ResourceHandle handle,
10
const Ogre::String& resource_group,
11
bool manual_load = false,
12
Ogre::ManualResourceLoader* pLoader = 0);
13
~Track ();
14
15
// Manejo bsico del track.
16
void play (int loop = -1);
17
void pause ();
18
void stop ();
19
20
void fadeIn (int ms, int loop);
21
void fadeOut (int ms);
22
static bool isPlaying ();
23
24
private:
25
// Funcionalidad de Ogre::Resource.
26
void loadImpl ();
27
void unloadImpl ();
28
size_t calculateSize () const;
29
30
// Variables miembro.
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.
Figura 6.14: La manipulacin bsica de sonido incluye las tpicas operaciones de play, pause y stop.
[189]
Por otra parte, las funciones de manejo bsico del track (lneas 16-22 ) son en
realidad una interfaz para manejar de una manera adecuada la biblioteca SDL_mixer.
Por ejemplo, la funcin miembro play simplemente interacta con SDL para comprobar si la cancin estaba pausada y, en ese caso, reanudarla. Si la cancin no estaba
pausada, entonces dicha funcin la reproduce desde el principio. Note cmo se hace
uso del gestor de logs de Ogre3D para almacenar en un archivo la posible ocurrencia
de algn tipo de error.
void
Track::play
(int loop)
{
Ogre::LogManager* pLogManager =
Ogre::LogManager::getSingletonPtr();
if(Mix_PausedMusic()) // Estaba pausada?
Mix_ResumeMusic(); // Reanudacin.
C6
31
Mix_Music* _pTrack; // SDL
32
Ogre::String _path; // Ruta al track.
33
size_t _size;
// Tamao.
34 };
[190]
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
void
Track::unloadImpl()
{
if (_pTrack) {
// Liberar el recurso de sonido.
Mix_FreeMusic(_pTrack);
}
}
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.
resource
ptr
refs
ptr
references
refs
[191]
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
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
losofa de diseo es exactamente igual que la planteada en esta seccin.
POW!
Figura 6.17: La integracin de
efectos de sonido es esencial para
dotar de realismo la parte sonora del
videojuego y complementar la parte
grca.
C6
[192]
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 ins-
[193]
C6
[194]
Ogre::
FrameListener
TrackManager
1
1
SoundFXManager
MyApp
Figura 6.18: Diagrama simplicado de clases de las principales entidades utilizadas para llevar a cabo la
integracin de msica y efectos de sonido.
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 }
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;
_root->startRendering();
return 0;
MyFrameListener
[195]
12
13
14
15
16
17
_keyboard->capture();
// Captura de las teclas de fecha...
if(_keyboard->isKeyDown(OIS::KC_1)) {
_sceneManager->setShadowTechnique(Ogre::
SHADOWTYPE_TEXTURE_MODULATIVE);
_shadowInfo = "TEXTURE_MODULATIVE";
_pMyApp->getSoundFXPtr()->play(); // REPRODUCCIN.
}
if(_keyboard->isKeyDown(OIS::KC_2)) {
_sceneManager->setShadowTechnique(Ogre::
SHADOWTYPE_STENCIL_MODULATIVE);
_shadowInfo = "STENCIL_MODULATIVE";
_pMyApp->getSoundFXPtr()->play(); // REPRODUCCIN.
}
18
19
20
21
22
// Tratamiento del resto de eventos...
23 }
6.3.
El gestor de recursos hace un uso extensivo del sistema de archivos. Tpicamente, en los PCs los sistemas de archivos son accesibles mediante llamadas al sistema,
proporcionadas por el propio sistema operativo. Sin embargo, en el mbito de los motores de juegos se suelen plantear esquemas ms generales y escalables debido a la
diversidad de plataformas existente para la ejecucin de un juego.
david
El sistema de archivos
home
lib
luis
andrea
media
Figura 6.20: Generalmente, los sistemas de archivos mantienen estructuras de rbol o de grafo.
Esta idea, discutida anteriormente a lo largo del curso, se basa en hacer uso de
wrappers o de capas software adicionales para considerar aspectos clave como el desarrollo multiplataforma. El planteamiento ms comn consiste en envolver la API del
sistema de archivos con una API especca del motor de juegos con un doble objetivo:
En el desarrollo multiplataforma, esta API especca proporciona el nivel de
abstraccin necesario para no depender del sistema de archivos y de la plataforma subyacente.
C6
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 simplicacin, en la propia clase FrameListener; en concreto,
en la funcin miembro frameStarted a la hora de capturar los eventos de teclado.
[196]
6.3.1.
El sistema de archivos dene la organizacin de los datos y proporciona mecanismos para almacenar, recuperar y actualizar informacin. As mismo, tambin es el
encargado de gestionar el espacio disponible en un determinado dispositivo de almacenamiento. Idealmente, el sistema de archivos ha de organizar los datos de una manera
eciente, teniendo en cuenta incluso las caractersticas especcas del dispositivo de
almacenamiento.
En el mbito de un motor de juegos, la API vinculada a la gestin del sistema de
archivos proporciona la siguiente funcionalidad [42]:
Manipular nombres de archivos y rutas.
Abrir, cerrar, leer y escribir sobre archivos.
Figura 6.21: Tras acabar con el formato HD-DVD, el Blu-Ray suceder al formato DVD como principal
alternativa en el soporte fsico de las
consolas de videojuegos de sobremesa.
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 prejo
/dev_bdvd para referirse al lector de blu-ray, mientras que el prejo /dev_hddx permite el acceso a un determinado disco duro.
Las rutas o paths pueden ser absolutas o relativas, en funcin de si estn denidas
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 dene en base al directorio
raz. Por otra parte, la segunda sera una ruta relativa, ya que se dene en relacin al
directorio /home/david.
Encapsulacin
Una vez ms, el principio de encapsulacin resulta fundamental para
abstraerse de la complejidad asociada al tratamiento del sistema de archivos y del sistema operativo subyacente.
[197]
C6
bin
boot
dev
etc
home
lib
david
luis
andrea
media
usr
bin
var
include
Antes de pasar a discutir aspectos de la gestin de E/S, resulta importante comentar la existencia de APIs especcas 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 especcas 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.
Instalacin
La instalacin de la biblioteca
Boost.Filesystem en sistemas Debian y derivados es trivial mediante cualquier gestor de paquetes. Una
bsqueda con apt-cache search es
todo lo que necesitas para averiguar
qu paquete/s hay que instalar.
[198]
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
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.
6.3.2.
E/S bsica
Para conseguir que la E/S asociada a un motor de juegos sea eciente, resulta imprescindible plantear un esquema de memoria intermedia que permita gestionar tanto
los datos escritos (ledos) como el destino en el que dichos datos se escriben (leen). Tpicamente, la solucin a este problema est basada en el uso de buffers. Bsicamente,
un buffer representa un puente de comunicacin entre el propio programa y un archivo
en disco. En lugar de, por ejemplo, realizar la escritura byte a byte en un archivo de
texto, se suelen emplear buffers intermedios para reducir el nmero de escrituras en
disco y, de este modo, mejorar la eciencia de un programa.
En esta seccin se prestar especial atencin a la biblioteca estndar de C, ya que
es la ms comnmente utilizada para llevar a cabo la gestin de E/S en el desarrollo
de videojuegos.
[199]
La diferencia entre ambas reside en que la API con E/S mediante buffer gestiona de
manera automtica el uso de los propios buffers sin necesidad de que el programador
asuma dicha responsabilidad. Por el contrario, la API de C que no proporciona E/S con
buffers tiene como consecuencia directa que el programador ha de asignar memoria
para dichos buffers y gestionarlos. La tabla 6.1 muestra las principales operaciones de
dichas APIs.
Listado 6.24: Ejemplo de operacin de E/S sncrona
1 #include <stdio.h>
2
3 int lectura_sincrona (const char* archivo, char* buffer,
4
size_t tamanyo_buffer, size_t* p_bytes_leidos);
5
6 int main (int argc, const char* argv[]) {
7
char buffer[256];
8
size_t bytes_leidos = 0;
9
10
if (lectura_sincrona("test.txt", buffer, sizeof(buffer), &
bytes_leidos))
11
printf(" %u bytes leidos!\n", bytes_leidos);
12
13
return 0;
14 }
15
16 int lectura_sincrona (const char* archivo, char* buffer,
17
size_t tamanyo_buffer, size_t* p_bytes_leidos) {
18
FILE* manejador = NULL;
19
20
if ((manejador = fopen(archivo, "rb"))) {
21
// Llamada bloqueante en fread,
22
// hasta que se lean todos los datos.
23
size_t bytes_leidos = fread(buffer, 1, tamanyo_buffer,
manejador);
24
25
// Ignoramos errores...
26
fclose(manejador);
27
28
*p_bytes_leidos = bytes_leidos;
29
return 1;
30
}
31
return -1;
32 }
Sender
Receiver
recibir (msg)
Procesar
mensaje
Figura 6.23: El uso de invocaciones sncronas tiene como consecuencia el bloqueo de la entidad que
realiza la invocacin.
Dependiendo del sistema operativo en cuestin, las invocaciones sobre operaciones de E/S se traducirn en llamadas nativas al sistema operativo, como ocurre en
el caso de UNIX y variantes, o en envolturas sobre alguna otra API de ms bajo nivel, como es el caso de sistemas Microsoft WindowsTM . Es posible aprovecharse del
hecho de utilizar las APIs de ms bajo nivel, ya que exponen todos los detalles de
implementacin del sistema de archivos nativo [42].
Una opcin relevante vinculada al uso de buffers consiste en gestionarlos para
reducir el impacto que la generacin de archivos de log produce. Por ejemplo, se puede
obtener un resultado ms eciente si antes de volcar informacin sobre un archivo en
disco se utiliza un buffer intermedio. As, cuando ste se llena, entonces se vuelca
sobre el propio archivo para mejorar el rendimiento de la aplicacin. En este contexto,
se puede considerar la delegacin de esta tarea en un hilo externo al bucle principal
del juego.
C6
El lenguaje de programacin C permite manejar dos APIs para gestionar las operaciones ms relevantes en relacin al contenido de un archivo, como la apertura, lectura,
escritura o el cierre. La primera de ellas proporciona E/S con buffers mientras que la
segunda no.
[200]
6.3.3.
E/S asncrona
[201]
Operacin
Abrir archivo
Cerrar archivo
Leer archivo
Escribir archivo
Desplazar
Obtener offset
Lectura lnea
Escritura lnea
Lectura cadena
Escritura cadena
Obtener estado
Operacin
Abrir archivo
Cerrar archivo
Leer archivo
Escribir archivo
Desplazar
Obtener offset
Lectura lnea
Escritura lnea
Lectura cadena
Escritura cadena
Obtener estado
Cuadro 6.1: Resumen de las principales operaciones de las APIs de C para la gestin de E/S (con y sin buffers).
La gura 6.24 muestra de manera grca cmo dos agentes se comunican de manera asncrona mediante un objeto de retrollamada. Bsicamente, el agente noticador
enva un mensaje al agente receptor de manera asncrona. Esta invocacin tiene dos
parmetros: i) el propio mensaje m y ii) un objeto de retrollamada cb. Dicho objeto
ser usado por el agente receptor cuando ste termine de procesar el mensaje. Mientras tanto, el agente noticador continuar su ujo normal de ejecucin, sin necesidad
de bloquearse por el envo del mensaje.
Callbacks
Las funciones de retrollamada se
suelen denominar comnmente
callbacks. Este concepto no slo
se usa en el mbito de la E/S asncrona, sino tambin en el campo
de los sistemas distribuidos y los
middlewares de comunicaciones.
La mayora de las bibliotecas de E/S asncrona existentes permiten que el programa principal espera una cierta cantidad de tiempo antes de que una operacin de E/S
se complete. Este planteamiento puede ser til en situaciones en las que los datos de
dicha operacin se necesitan para continuar con el ujo normal de trabajo. Otras APIs
pueden proporcionar incluso una estimacin del tiempo que tardar en completarse
una operacin de E/S, con el objetivo de que el programador cuente con la mayor
cantidad posible de informacin. As mismo, tambin es posible encontrarse con operaciones que asignen deadlines sobre las propias peticiones, con el objetivo de plantear
un esquema basado en prioridades. Si el deadline se cumple, entonces tambin suele
ser posible asignar el cdigo a ejecutar para contemplar ese caso en particular.
C6
[202]
Agente 1
Notificador
: Agente
Receptor
<<crear ()>>
: Objeto
callback
recibir_mensaje (cb, m)
Procesar
mensaje
responder ()
responder ()
Figura 6.24: Esquema grco representativo de una comunicacin asncrona basada en objetos de retrollamada.
[203]
wait
signal
7 http://www.boost.org/doc/libs/1_48_0/doc/html/boost_asio.html
8 La nocin de segundo plano en este contexto est vinculada a independizar ciertas tareas de la ejecucin
del ujo principal de un programa.
C6
6.3.4.
[204]
#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;
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 denir manejadores de cdigo asociados a funciones de retrollamada que se ejecuten mientras el
programa contina la ejecucin de su ujo principal. Asio tambin proporciona mecanismos para que el programa no termine mientras haya tareas por nalizar.
El siguiente listado de cdigo muestra cmo el ejemplo anterior se puede modicar
para llevar a cabo una espera asncrona, asociando en este caso un temporizador que
controla la ejecucin de una funcin de retrollamada.
Listado 6.26: Biblioteca Asio. Espera asncrona.
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 <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();
In the meantime...
Recuerde que mientras se lleva a
cabo una espera asncrona es posible continuar ejecutando operaciones en el hilo principal. Aproveche
este esquema para obtener un mejor
rendimiento en sistemas con ms de
un procesador.
[205]
25
26
return 0;
27 }
$ Esperando a print()...
$ Hola Mundo!
Sin embargo, pasarn casi 3 segundos entre la impresin de una secuenciay otra,
espera
ejecutar la primera secuencia de impresin
17 ), mientras que la ejecucin de
(lnea
#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);
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));
// ...
}
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.
Flexibilidad en Asio
La biblioteca Boost.Asio es muy
exible y posibilita la llamada asncrona a una funcin que acepta un
nmero arbitrario de parmetros.
stos pueden ser variables, punteros, o funciones, entre otros.
C6
[206]
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;
}
_timer1.async_wait(_strand.wrap
(boost::bind(&Counter::count1, this)));
return 0;
6.3.5.
Consideraciones nales
En el siguiente captulo se discutir otro enfoque basado en la concurrencia mediante hilos en C++ para llevar a cabo tareas como por ejemplo la carga de contenido,
en segundo plano, de un juego. En dicho captulo se utilizar como soporte la biblioteca de utilidades del middleware ZeroC I CE, el cual tambin se estudiar en el mdulo
3, Tcnicas Avanzadas de Desarrollo.
6.4.
En esta seccin se justica 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 denicin de datos multimedia, en la
prctica cada aplicacin grca interactiva tiene necesidades especcas 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 codicar 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.
C6
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[207]
[208]
XML mantiene un alto nivel semntico, est bien soportado y estandarizado y permite
asociar estructuras de rbol bien denidas para encapsular la informacin relevante.
Adems, posibilita que el contenido a importar sea legible por los propios programadores.
6.4.1.
Formatos de intercambio
Ogre Exporter
El proceso de importacin en Ogre3D est directamente relacionado con el formato XML a travs de una serie de estructuras bien denidas para manejar escenas,
mallas o esqueletos, entre otros.
Aunque el uso del metalenguaje XML es una de las opciones ms extendidas, a
continuacin se discute brevemente el uso de otras alternativas.
Importer
Archivos binarios
Este esquema basado en un formato binario es muy eciente 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 denicin 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.
9 http://www.ogre3d.org/tikiwiki/OGRE+Exporters
10 http://www.ogre3d.org/tikiwiki/OgreXmlConverter
Figura 6.28: Procesos de importancin y exportacin de datos 3D haciendo uso de documentos XML.
[209]
Es un estndar.
Est muy bien soportado, tanto a nivel de programacin como a nivel de usuario.
HTML
XML
XHTML
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
propietarios, los cuales son ms rpidos de parsear y mucho ms compactos que los
archivos XML, reduciendo as los tiempos de importacin y de carga.
El parser Xerces-C++
Como se ha comentado anteriormente, una de los motivos por los que XML est
tan extendido es su amplio soporte a nivel de programacin. En otras palabras, prcticamente la mayora de lenguajes de programacin proporcionan bibliotecas, APIs y
herramientas para procesar y generar contenidos en formato XML.
C6
[210]
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++ proporcionar una biblioteca con funcionalidad para parsear, generar,
manipular y validar documentos XML utilizando las APIs DOM, SAX (Simple API
for XML) y SAX2. Estas APIs son las ms populares para manipular documentos
XML y dieren, sin competir entre s, en aspectos como el origen, alcance o estilo de
programacin.
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
6.4.2.
apt-get
apt-get
apt-get
apt-get
update
install libxerces-c-dev
install libxerces-c-doc
install libxerces-samples
Creacin de un importador
[211]
C6
Figura 6.32: 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.
El propio escenario tridimensional, considerando los puntos o nodos de generacin y destruccin de fantasmas.
Los propios fantasmas.
Una o ms cmaras virtuales, que servirn para visualizar el juego. Cada cmara tendr asociado un camino o path, compuesto a su vez por una serie de
puntos clave que indican la situacin (posicin y rotacin) de la cmara en cada
momento.
El nodo raz
La etiqueta <data> es el nodo raz
del documento XML. Sus posibles
nodos hijo estn asociados con las
etiquetas <graph> y <camera>.
En principio, la informacin del escenario y de las cmaras virtuales ser importada al juego, haciendo uso del importador cuyo diseo se discute a continuacin. Sin
embargo, antes se muestra un posible ejemplo de la representacin de estos mediante
el formato denido para la demo en cuestin.
En concreto, el siguiente listado muestra la parte de informacin asociada a la
estructura de grafo que conforma el escenario. Como se puede apreciar, la estructura
graph est compuesta por una serie de vrtices (vertex) y de arcos (edge).
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 identica unvocamente, y un tipo, el cual puede ser spawn (punto
de generacin) o drain (punto de desaparicin), respectivamente. Por otra parte, los
arcos permiten denir la estructura concreta del grafo a importar mediante la etiqueta
<vertex>.
Vrtices y arcos
En el formato denido, los vrtices
se identican a travs del atributo
index. Estos IDs se utilizan en la etiqueta <edge> para especicar los
dos vrtices que conforman un arco.
En caso de que sea necesario incluir ms contenido, simplemente habr que extender el formato denido. XML facilita enormemente la escalabilidad gracias a su
esquema basado en el uso de etiquetas y a su estructura jerrquica.
[212]
Listado 6.30: Ejemplo de chero XML usado para exportar e importar contenido asociado al
grafo del escenario.
1 <?xml version=1.0 encoding=UTF-8?>
2 <data>
3
4
<graph>
5
6
<vertex index="1" type="spawn">
7
<x>1.5</x> <y>2.5</y> <z>-3</z>
8
</vertex>
9
<!-- More vertexes... -->
10
<vertex index="4" type="drain">
11
<x>1.5</x> <y>2.5</y> <z>-3</z>
12
</vertex>
13
14
<edge>
15
<vertex>1</vertex> <vertex>2</vertex>
16
</edge>
17
<edge>
18
<vertex>2</vertex> <vertex>4</vertex>
19
</edge>
20
<!-- More edges... -->
21
22
</graph>
23
24
<!-- Definition of virtual cameras -->
25 </data>
La clase Scene
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.
[213]
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Node.h>
#include <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 identicador de uno de ellos (lnea 17 ).
C6
Listado 6.31: Ejemplo de chero XML usado para exportar e importar contenido asociado a
las cmaras virtuales.
[214]
Scene
1
Importer
Ogre::Singleton
1..*
Camera
Graph
1..*
GraphVertex
2
1
1..*
Frame
0..*
0..*
GraphEdge
Figura 6.33: Diagrama de clases de las entidades ms relevantes del importador de datos.
#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;
1
Node
[215]
26
std::vector<GraphEdge*> _edges;
27 };
(drain)
17
Ogre::Vector3 _position; // Posicin del nodo en el espacio 3D
18 };
GraphVertex y Node
La clase GraphVertex mantiene como variable de clase un objeto de tipo Node, el cual alberga la informacin de un nodo en el espacio 3D.
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 identicador, 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.
La clase Importer
#include <OGRE/Ogre.h>
#include <xercesc/dom/DOM.hpp>
#include <Scene.h>
class Importer: public Ogre::Singleton<Importer> {
13 Se
C6
[216]
6
public:
7
// nica funcin miembro pblica para parsear.
8
void parseScene (const char* path, Scene *scn);
9
10
static Importer& getSingleton ();
// Ogre::Singleton.
11
static Importer* getSingletonPtr (); // Ogre::Singleton.
12
13
private:
14
// Funcionalidad oculta al exterior.
15
// Facilita el parseo de las diversas estructuras
16
// del documento XML.
17
void parseCamera (xercesc::DOMNode* cameraNode, Scene* scn);
18
19
void addPathToCamera (xercesc::DOMNode* pathNode, Camera *cam);
20
void getFramePosition (xercesc::DOMNode* node,
21
Ogre::Vector3* position);
22
void getFrameRotation (xercesc::DOMNode* node,
23
Ogre::Vector4* rotation);
24
25
void parseGraph (xercesc::DOMNode* graphNode, Scene* scn);
26
void addVertexToScene (xercesc::DOMNode* vertexNode, Scene* scn);
27
void addEdgeToScene (xercesc::DOMNode* edgeNode, Scene* scn);
28
29
// Funcin auxiliar para recuperar valores en punto flotante
30
// asociados a una determinada etiqueta (tag).
31
float getValueFromTag (xercesc::DOMNode* node, const XMLCh *tag);
32 };
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 // SE OMITE PARTE DEL CDIGO FUENTE (bloques try-catch)
2 void Importer::parseScene (const char* path, Scene *scene) {
3
// Inicializacin.
4
XMLPlatformUtils::Initialize();
5
6
XercesDOMParser* parser = new XercesDOMParser();
7
parser->parse(path);
8
9
DOMDocument* xmlDoc;
10
DOMElement* elementRoot;
11
Estructurando cdigo...
Una buena programacin estructura
es esencial para facilitar el mantenimiento del cdigo y balancear de
manera adecuada la complejidad de
las distintas funciones que forman
parte del mismo.
C6
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 }
[217]
Captulo
ste captulo realiza un recorrido por los sistemas de soporte de bajo nivel necesarios para efectuar diversas tareas crticas para cualquier motor de juegos.
Algunos ejemplos representativos de dichos subsistemas estn vinculados al
arranque y la parada del motor, su conguracin y a la gestin de cuestiones de ms
bajo nivel.
El objetivo principal del presente captulo consiste en profundizar en dichas tareas
y en proporcionar al lector una visin ms detallada de los subsistemas bsicos sobre
los que se apoyan el resto de elementos del motor de juegos.
From the ground up!
Los subsistemas de bajo nivel del
motor de juegos resultan esenciales
para la adecuada integracin de elementos de ms alto nivel. Algunos
de ellos son simples, pero la funcionalidad que proporcionan es crtica
para el correcto funcionamiento de
otros subsistemas.
[220]
CPU
CPU
L1 Cach
L1 Cach
L2 Cach
CPU, ha permitido que aplicaciones tan exigentes como los videojuegos se pueden
7.1.
7.1.1.
Aspectos fundamentales
El subsistema de arranque y parada es el responsable de llevar a cabo la inicializacin y conguracin de los distintos subsistemas que forman parte del motor de
juegos, as como de realizar la parada de los mismos cuando as sea necesario. Este
sistema forma parte de la capa de subsistemas principales que se introdujo brevemente
en la seccin 1.2.4. La gura 7.2 muestra la interaccin del subsistema de arranque y
parada con el resto de componentes de la arquitectura general del motor de juegos.
Este subsistema juega un papel bsico pero fundamental dentro de la arquitectura del motor de juegos, ya que disponer de una entidad software que conozca las
interdependencias entre el resto de subsistemas es crucial para efectuar su arranque,
conguracin y parada de una manera adecuada. Desde un punto de vista general, si
un subsistema S tiene una dependencia con respecto a un subsistema T , entonces el
subsistema de arranque ha de tener en cuenta dicha dependencia para arrancar primero
T y, a continuacin, S. As mismo, la parada de dichos subsistemas se suele realizar
generalmente en orden inverso, es decir, primero se parara S y, posteriormente, T
(ver gura 7.3).
L1 Cach
L1 Cach
CPU
CPU
Subsistema T
Subsistema U
Subsistema U
Orden
de
arranque
Subsistema T
Subsistema S
Subsistema S
Orden
de
parada
Subsistema T
Subsistema U
[221]
Gestin de
memoria
Gestin de
archivos
Biblioteca
matemtica
...
Subsistemas principales
Variables globales
Recuerde que idealmente las variables globales se deberan limitar en
la medida de lo posible con el objetivo de evitar efectos colaterales.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MemoryManager {
public:
MemoryManager () {
// Inicializacin gestor de memoria...
}
~MemoryManager () {
// Parada gestor de memoria...
}
// ...
};
// Instancia nica.
static MemoryManager gMemoryManager;
C7
Sistema de
arranque y parada
[222]
Debido a que C++ es el lenguaje estndar para el desarrollo de videojuegos modernos, una posible alternativa para gestionar de manera correcta tanto el arranque como
la parada de subsistemas podra consistir en hacer uso de los mecanismos nativos de
C++ de construccin y destruccin de elementos.
Desde un punto de vista general, los objetos globales y estticos se instancian de
manera previa a la ejecucin de la funcin main. Del mismo modo, dichos objetos se
destruyen de manera posterior a la ejecucin de dicha funcin, es decir, inmediatamente despus de su retorno. Sin embargo, tanto la construccin como la destruccin
de dichos objetos se realizan en un orden impredecible.
La consecuencia directa del enfoque nativo proporcionado por C++ para la construccin y destruccin de objetos es que no se puede utilizar como solucin directa
para arrancar y parar los subsistema del motor de juegos. La razn se debe a que
no es posible establecer un orden que considere las posibles interdependencias entre
subsistemas.
Listado 7.2: Subsistema de gestin de memoria con acceso a instancia nica.
1 class MemoryManager {
2 public:
3
static MemoryManager& get () {
4
static MemoryManager gMemoryManager;
5
return gMemoryManager;
6
}
7
MemoryManager () {
8
// Arranque de otros subsistemas dependientes...
9
SubsistemaX::get();
10
SubsistemaY::get();
11
12
// Inicializacin gestor de memoria...
13
}
14
~MemoryManager () {
15
// Parada gestor de memoria...
16
}
17
// ...
18 };
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++.
[223]
Adems, este enfoque presenta el inconveniente asociado al momento de la construccin de la instancia global del subsistema de memoria. Evidentemente, la construccin se efectuar a partir de la primera llamada a la funcin get, pero es complicado
controlar cundo se efectuar dicha llamada.
Por otra parte, no es correcto realizar suposiciones sobre la complejidad vinculada
a la obtencin del singleton, ya que los distintos subsistemas tendrn necesidades muy
distintas entre s a la hora de inicializar su estado, considerando las interdependencias
con otros subsistemas. En general, este diseo puede ser problemtico y es necesario proporcionar un esquema sobre el que se pueda ejercer un mayor control y que
simplique los procesos de arranque y parada.
MemoryManager
t ()
Funcin main ()
startUp ()
Funcin main ()
Figura 7.4: Esquema grco de obtencin de un singleton y su posterior utilizacin.
Interdependencias
El orden del arranque, inicializacin y parada de subsistemas dependern de las relaciones entre los
mismos. Por ejemplo, el subsistema de gestin de memoria ser tpicamente uno de los primeros en
arrancarse, debido a que gran parte
del resto de subsistemas tienen una
dependencia directa respecto al primero.
7.1.2.
En esta subseccin se estudia un enfoque simple [42], aunque ampliamente utilizado, para gestionar tanto el arranque como la parada de los distintos subsistemas que
forman la arquitectura de un motor de juegos. La idea principal de este enfoque reside
en explicitar el arranque y la parada de dichos subsistemas, que a su vez se gestionan
mediante managers implementados de acuerdo al patrn singleton.
Para ello, tanto el arranque como la parada de un subsistema se implementa mediante funciones explcitas de arranque y parada, tpicamente denominadas startUp
y shutDown, respectivamente. En esencia, estas funciones representan al constructor
y al destructor de la clase. Sin embargo, la posibilidad de realizar llamadas permite
controlar de manera adecuada la inicializacin y parada de un subsistema.
El siguiente listado de cdigo muestra la implementacin tpica de un subsistema
de gestin haciendo uso del enfoque planteado en esta subseccin. Observe cmo tanto
el constructor y el destructor de clase estn vacos, ya que se delega la inicializacin
del subsistema en las funciones startUp() y shutDown().
Listado 7.4: Implementacin de funciones startUp() y shutDown().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MemoryManager {
public:
MemoryManager () {}
~MemoryManager () {}
void
//
}
void
//
}
};
startUp () {
Inicializacin gestor de memoria...
shutDown () {
Parada gestor de memoria...
C7
No obstante, aunque existe cierto control sobre el arranque de los subsistemas dependientes de uno en concreto, esta alternativa no proporciona control sobre la parada
de los mismos. As, es posible que C++ destruya uno de los subsistemas dependientes
del gestor de memoria de manera previa a la destruccin de dicho subsistema.
[224]
Mediante este enfoque, la inicializacin y parada de subsistemas es trivial y permite un mayor control sobre dichas tareas. En el siguiente listado de cdigo se muestran
de manera explcita las dependencias entre algunos de los subsistemas tpicos de la
arquitectura de motor, como por ejemplo los subsistemas de gestin de memoria, manejo de texturas, renderizado o animacin.
7.1.3.
Aunque Ogre 3D es en realidad un motor de renderizado en lugar de un completo motor de juegos, dicho entorno proporciona una gran cantidad de subsistemas
relevantes para facilitar el desarrollo de videojuegos. Entre ellos, tambin existe un
subsistema de arranque y parada que hace gala de una gran simplicidad y sencillez.
Bsicamente, el arranque y la parada en Ogre se realiza a travs del uso de la clase
Ogre::Root, la cual implementa el patrn singleton con el objetivo de asegurar una
nica instancia de dicha clase, la cual acta como punto central de gestin.
Para ello, la clase Root almacena punteros, como variables miembro privadas, a
todos los subsistemas soportados por Ogre con el objetivo de gestionar su creacin y
su destruccin. El siguiente listado de cdigo muestra algunos aspectos relevantes de
la declaracin de esta clase.
Ogre::Singleton
La clase Ogre::Singleton est basada en el uso de plantillas para poder
instanciarla para tipos particulares.
Dicha clase proporciona las tpicas
funciones getSingleton() y getSingletonPtr para acceder a la referencia y al puntero, respectivamente,
de la nica instancia de clase creada.
[225]
RootAlloc
Ogre::Root
Figura 7.6: Clase Ogre::Root y su relacin con las clases Ogre::Singleton y RootAlloc.
mRenderSystemCapabilitiesManager;
ScriptCompilerManager *mCompilerManager;
// Ms declaraciones...
C7
Ogre::Singleton<Root>
[226]
configFileName,
2
const String& logFileName)
3 // Inicializaciones...
4 {
5
// Comprobacin del singleton en clase padre.
6
// Inicializacin.
7
// src ...
8
9
// Creacin del log manager y archivo de log por defecto.
10
if(LogManager::getSingletonPtr() == 0) {
11
mLogManager = OGRE_NEW LogManager();
12
mLogManager->createLog(logFileName, true, true);
13
}
14
15
// Gestor de biblioteca dinmica.
16
mDynLibManager = OGRE_NEW DynLibManager();
17
mArchiveManager = OGRE_NEW ArchiveManager();
18
19
// ResourceGroupManager.
20
mResourceGroupManager = OGRE_NEW ResourceGroupManager();
21
22
// src...
23
24
// Material manager.
25
mMaterialManager = OGRE_NEW MaterialManager();
26
// Mesh manager.
27
mMeshManager = OGRE_NEW MeshManager();
28
// Skeleton manager.
29
mSkeletonManager = OGRE_NEW SkeletonManager();
30
// Particle system manager.
31
mParticleManager = OGRE_NEW ParticleSystemManager();
32
// src...
33 }
[227]
) {
1 https://github.com/id-Software/Quake-III-Arena
C7
7.1.4.
[228]
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.
vwMain ()
G_InitGame ()
G_RunFrame ()
G_ShutdownGame ()
Figura 7.8: Visin abstracta del ujo general de ejecucin de Quake III
Arena (no se considera la conexin
y desconexin de clientes).
7.2. Contenedores
[229]
Como se puede apreciar en la funcin de parada, bsicamente se escribe informacin en el chero de log y se almacenan los datos de las sesiones de
para,
los clientes
posteriormente, permitir su recuperacin. Note cmo en las lneas 14-15 se delega la
parada de los bots, es decir, de los NPCs en la funcin BotAIShutdown(). De nuevo, el
esquema planteado se basa en delegar la parada en distintas funciones en funcin de
la entidad afectada.
Programacin estructura
El cdigo de Quake es un buen
ejemplo de programacin estructurada que gira en torno a una adecuada denicin de funciones y a un
equilibrio respecto a la complejidad
de las mismas.
7.2.
Contenedores
C7
13
BotAIShutdown( restart );
14
}
15 }
[230]
Acceso
directo
Bidireccional
Unidireccional
Entrada
Salida
Figura 7.9: Esquema grca 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.
En esta seccin se introducirn algunos aspectos fundamentales que no se discutieron anteriormente y se mencionarn algunas bibliotecas tiles para el uso de algunas
estructuras de datos que son esenciales en el desarrollo de videojuegos, como por
ejemplo los grafos. Estos aspectos se estudiarn con mayor profundidad en el mdulo
3, Tcnicas Avanzadas de Desarrollo.
7.2.1.
Iteradores
Desde un punto de vista abstracto, un iterador se puede denir como una clase
que permite acceder de manera eciente a los elementos de un contenedor especco.
Segn [94], un iterador es una abstraccin pura, es decir, cualquier elemento que se
comporte como un iterador se dene como un iterador. En otras palabras, un iterador es una abstraccin del concepto de puntero a un elemento de una secuencia. Los
principales elementos clave de un iterador son los siguientes:
Patrn Iterator
Normalmente, los iteradores se implementan haciendo uso del patrn que lleva su mismo nombre,
tal y como se discuti en la seccin 4.4.3.
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.
7.2. Contenedores
[231]
Categoras de iteradores y operaciones
Salida Entrada Unidireccional Bidireccional
=*p
=*p
=*p
->
->
->
*p=
*p=
*p=
++
++
++
++
== !=
== !=
== !=
Acceso directo
=*p
->[]
*p=
++ + - += -=
== != <><= >=
Cuadro 7.1: Resumen de las principales categoras de iteradores en STL junto con sus operaciones [94].
No olvide implementar el constructor de copia y denir el operador de asignacin para el tipo que se utilizar en un contenedor y que ser manipulado
mediante iteradores, ya que las lecturas y escrituras copian objetos.
Los iteradores tambin se pueden aplicar sobre ujos 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 ujo.
istreambuf_iterator, para leer de un buffer de ujo.
7.2.2.
Ms all de STL
C7
Categora
Lectura
Acceso
Escritura
Iteracin
Comparacin
[232]
Listado 7.12: Ejemplo de uso de iteradores para llevar a cabo operaciones de E/S.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <iterator>
using namespace std;
int main () {
// Escritura de enteros en cout.
ostream_iterator<int> fs(cout);
*fs = 7;
++fs;
*fs = 6;
return 0;
}
Control total sobre los contenedores y estructuras de datos desarrolladas, especialmente sobre el mecanismo de asignacin de memoria, aunque sin olvidar
aspectos como los propios algoritmos. Aunque STL permite crear asignadores
de memoria personalizados (custom allocators), en ocasiones los propios patrones de los contenedores de STL pueden ser insucientes.
Optimizaciones, considerando el propio hardware sobre el que se ejecutar el
motor. STL es un estndar independiente de la plataforma y el sistema operativo,
por lo que no es posible aplicar optimizaciones de manera directa sobre la propia
biblioteca.
Personalizacin debido a la necesidad de incluir funcionalidad sobre un contenedor que no est inicialmente considerada en otras bibliotecas como STL.
Por ejemplo, en un determinado problema puede ser necesario obtener los n
elementos ms adecuados para satisfacer una necesidad.
Independencia funcional, ya que es posible depurar y arreglar cualquier problema sin necesidad de esperar a que sea solucionado por terceras partes.
De manera independiente al hecho de considerar la opcin de usar STL, existen
otras bibliotecas relacionadas que pueden resultar tiles para el desarrollador de videojuegos. Una de ellas es STLPort, una implementacin de STL especcamente
ideada para ser portable a un amplio rango de compiladores y plataformas. Esta implementacin proporciona una funcionalidad ms rica que la que se puede encontrar
en otras implementaciones de STL.
La otra opcin que se discutir brevemente en esta seccin es el proyecto Boost,
cuyo principal objetivo es extender la funcionalidad de STL. 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.
Estandarizando Boost
Gran parte de las bibliotecas del
proyecto Boost estn en proceso de
estandarizacin con el objetivo de
incluirse en el propio estndar de
C++.
7.2. Contenedores
[233]
1
2
B
2
12
E
3
C
D
C7
D
C
4
Figura 7.10: Representacin grca 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.
La gura 7.10 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.
Edsger W. Dijkstra
Una de las personas ms relevantes en el mbito de la computacin
es Dijkstra. Entre sus aportaciones,
destacan la solucin al camino ms
corto, la notacin polaca inversa, el
algoritmo del banquero, la denicin de semforo como mecanismo
de sincronizacin e incluso aspectos de computacin distribuida, entre otras muchas contribuciones.
[234]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <boost/config.hpp>
#include <iostream>
#include <boost/graph/graph_traits.hpp>
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/dijkstra_shortest_paths.hpp>
using namespace boost;
int
main(int, char *[])
{
// Definicin de estructuras de datos...
typedef adjacency_list <listS, vecS, directedS,
no_property, property <edge_weight_t, int> > graph_t;
typedef graph_traits <graph_t>::vertex_descriptor
vertex_descriptor;
17
typedef graph_traits <graph_t>::edge_descriptor edge_descriptor;
18
typedef std::pair<int, int> Edge;
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
5 http://www.boost.org/doc/libs/1_55_0/libs/graph/example/
dijkstra-example.cpp
El ltimo bloque de cdigo (lneas 44-52 ) permite obtener la informacin relevante a partir del clculo de los caminos mnimos desde A, es decir, la distancia que
existe desde el propio nodo A hasta cualquier otro y el nodo a partir del cual se accede
al destino nal (partiendo de A).
Para generar un ejecutable, simplemente es necesario enlazar con la biblioteca
boost_graph:
$ g++ dijkstra.cpp -o dijkstra -lboost_graph
$ ./dijkstra
cad
Al ejecutar, la salida del programa mostrar todos los caminos mnimos desde el
vrtice especicado; en este caso, desde el vrtice A.
Distancias y nodos padre:
'H'
p
'o'
'l'
'a'
'm'
'u'
'n'
'd'
'o'
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
7.3.1.
Cuestiones especcas
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.
typedef basic_string<char> string;
La consecuencia directa de esta implementacin es que el desarrollador ha de considerar cmo gestionar la reserva de memoria para dar soporte al uso de cadenas:
C7
45
46
47
48
49
50
51
52
53
54
55 }
[235]
[236]
Finalmente, el impacto de las operaciones tpicas de cadenas es muy importante con el objetivo de obtener un buen rendimiento. Por ejemplo, la comparacin de
cadenas mediante la funcin strcmp() tiene una complejidad lineal respecto a la longitudad de las cadenas, es decir, una complejidad O(n). Del mismo modo, la copia de
cadenas tambin tiene una complejidad lineal, sin considerar la posibilidad de reservar memoria de manera dinmica. Por el contrario, la comparacin de tipos de datos
ms simples, como los enteros, es muy eciente y est soportado por instrucciones
mquina.
7.3.2.
El uso de una clase string que abstraiga de la complejidad subyacente de la implementacin de datos textuales es sin duda el mecanismo ms prctico para trabajar con
cadenas. Por ejemplo, en el caso de C++, el programador puede utilizar directamente
la clase string6 de la biblioteca estndar. Dicha clase proporciona una gran cantidad
de funcionalidad, estructurada en cinco bloques bien diferenciados:
Iteradores, que permiten recorrer de manera eciente el contenido de las instancias de la clase string. Un ejemplo es la funcin begin(), que devuelve un
iterador al comienzo de la cadena.
6 http://www.cplusplus.com/reference/string/string/
Internationalization
La localizacin, en el mbito de
gestin de mltiples lenguajes, se
suele conocer como internationalization, o I18N, y es esencial para garantizar la inexistencia de problemas al tratar con diferentes idiomas.
[237]
Capacidad, que proporciona operaciones para obtener informacin sobre el tamao de la cadena, redimensionarla e incluso modicar la cantidad de memoria
reservada para la misma. Un ejemplo es la funcin size(), que devuelve la longitudad de la cadena.
Modicadores, que proporcionan la funcionalidad necesaria para alterar el contenido de la cadena de texto. Un ejemplo es la funcin swap(), que intercambia
el contenido de la cadena por otra especicada como parmetro.
Operadores de cadena, que dene operaciones auxiliares para tratar con cadenas. Un ejemplo es la funcin compare(), que permite comparar cadenas.
No obstante, tal y como se introdujo anteriormente, la abstraccin proporcionada
por este tipo de clases puede ocultar el coste computacional asociado a la misma y, en
consecuencia, condicionar el rendimiento del juego. Por ejemplo, pasar una cadena de
texto como parmetro a una funcin utilizando el estilo C, es decir, pasando un puntero
al primer elemento de la cadena, es una operacin muy eciente. Sin embargo, pasar
un objeto cadena puede ser ineciente debido a la generacin de copias mediante
los constructores de copia, posiblemente provocando reserva dinmica de memoria y
penalizando an ms el rendimiento de la aplicacin.
Proling strings!
El uso de herramientas de proling 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.
Como regla general, pase los objetos de tipo string como referencia y no
como valor, ya que esto incurrira en una penalizacin debido al uso de constructores de copia.
En el mbito del desarrollo de videojuegos, la identicacin de entidades o elementos est estrechamente ligada con el uso de cadenas de texto. Desde un punto de
vista general, la identicacin unvoca de objetos en el mundo virtual de un juego
es una parte esencial en el desarrollo como soporte a otro tipo de operaciones, como
por ejemplo la localizacin de entidades en tiempo de ejecucin por parte del motor
de juegos. As mismo, las entidades ms van all de los objetos virtuales. En realidad,
tambin abarcan mallas poligonales, materiales, texturas, luces virtuales, animaciones,
sonidos, etctera.
C7
Acceso, que permite obtener el valor de caracteres concretos dentro de la cadena. Un ejemplo es la funcin at(), que devuelve el carcter de la posicin
especicada como parmetro.
[238]
En este contexto, las cadenas de texto representan la manera ms natural de llevar a cabo dicha identicacin y tratamiento. Otra opcin podra consistir en manejar
una tabla con valores enteros que sirviesen como identicadores de las distintas entidadas. Sin embargo, los valores enteros no permiten expresar valores semnticos,
mientras que las cadenas de texto s.
Debido a la importancia del tratamiento y manipulacin de cadenas, las operaciones asociadas han de ser ecientes. Desafortunadamente, este criterio no se cumple en
el caso de funciones como strcmp(). La solucin ideal debera combinar la exibilidad
y poder descriptivo de las cadenas con la eciencia de un tipo de datos primitivo, como
los enteros. Este planteamiento es precisamente la base del esquema que se discute a
continuacin.
7.3.3.
Hashing de cadenas
El uso de una estructura asociativa (ver seccin 5.4) se puede utilizar para manejar de manera eciente cadenas textuales. La idea se puede resumir en utilizar una
estructura asociativa en la que la clave sea un valor numrico y la clase sea la propia
cadena de texto. De este modo, la comparacin entre los cdigos hash de las cadenas,
es decir, los enteros, es muy rpida.
Si las cadenas se almacenan en una tabla hash, entonces el contenido original se
puede recuperar a partir del cdigo hash. Este esquema es especialmente til en el
proceso de depuracin para mostrar el contenido textual, ya sea de manera directa por
un dispositivo de visualizacin o mediante los tpicos archivos de log.
Una de las principales cuestiones a considerar cuando se utilizan este tipo de planteamientos es el diseo de la funcin de hashing. En otras palabras, la funcin que
traduce las claves, representadas normalmente mediante tipos de datos numricos, en
valores, representados por cadenas textuales en esta seccin. El principal objetivo consiste en evitar colisiones, es decir, evitar que dos cadenas distintas tengan asociadas el
mismo cdigo o clave. Esta ltima situacin se dene como perfect hashing y existe
una gran cantidad de informacin en la literatura7 .
Respecto a la implementacin de un esquema basado en hashing de cadenas, uno
de los aspectos clave consiste en decidir cundo llamar a la funcin de hashing. Desde
un punto de vista general, la mayora de los motores de juegos permiten obtener el
identicador asociado a una cadena en tiempo de ejecucin. Sin embargo, es posible
aplicar algunos trucos para optimizar dicha funcionalidad. Por ejemplo, es bastante
comn hacer uso de macros para obtener el identicador a partir de una cadena para
su posterior uso en una sentencia switch.
Tambin es importante tener en cuenta que el resultado de una funcin de hashing se suele almacenar en algn tipo de estructura de datos, en ocasiones global, con
el objetivo de incrementar la eciencia en consultas posteriores. Este planteamiento, parecido a un esquema de cach para las cadenas textuales, permite mejorar el
rendimiento de la aplicacin. El siguiente listado de cdigo muestra una posible implementacin que permite el manejo de cadenas de texto mediante una estructura de
datos global.
Listado 7.14: Ejemplo de hashing de cadenas.
1 static StringId sid_hola = internString("hola");
2 static StringId sid_mundo = internString("mundo");
3
4 // ...
7 Se
recomienda la visualizacin del curso Introduction to Algorithms del MIT, en concreto las clases 7
y 8, disponible en la web.
String id
En el mbito profesional, el trmino
string id se utiliza comnmente para referirse a la cadena accesible
mediante un determinado valor o
cdigo hash.
[239]
C7
5
6 void funcion (StringId id) {
7
// Ms eficiente que...
8
// if (id == internString ("hola"))
9
if (id == sid_hola)
10
// ...
11
else if (id == sid_mundo)
12
// ...
13 }
7.4.
7.4.1.
[240]
Dentro del mbito de los cheros de conguracin, 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.
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 conguracin. Tpicamente, dicha informacin se
almacenaba en tarjetas de memoria externas que permitan salvar las partidas guardadas y la conguracin proporcionada por el usuario de un determinado juego.
Este planteamiento tiene como principal ventaja la eciencia 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.
El lenguaje XML
eXtensible Markup Language es un
metalenguaje extensible basado en
etiquetas que permite la denicin
de lenguajes especcos de un determinado dominio. Debido a su popularidad, existen multitud de herramientas que permiten el tratamiento y la generacin de archivos
bajo este formato.
Los propios registros del sistema operativo tambin se pueden utilizar como soporte al almacenamiento de parmetros de conguracin. 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 clavevalor.
Tambin es posible utilizar la lnea de rdenes o incluso la denicin de variables
de entorno para llevar a cabo la conguracin de un motor de juegos.
Finalmente, es importante reexionar 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, sacricando
el control de los datos por parte del usuario.
7.4.2.
Una tcnica bastante comn que est directamente relacionada con la conguracin de un motor de juegos reside en el uso de esquemas de denicin de datos.
Bsicamente, la idea general reside en hacer uso de algn tipo de lenguaje de programacin declarativo, como por ejemplo LISP, para llevar a cabo la denicin de
datos que permitan su posterior exportacin a otros lenguajes de programacin. Este
tipo de lenguajes son muy exibles y proporcionan una gran potencia a la hora de
denir estructuras de datos.
A continuacin se muestra un ejemplo real del videojuego Uncharted: Drakes
Fortune, desarrollado por Naughty Dog para la consola de sobremesa Playstation 3TM ,
y discutido en profundidad en [42]. En dicho ejemplo se dene una estructura bsica
para almacenar las propiedades de una animacin y dos instancias vinculadas a un tipo
particular de animacin, utilizando para ello un lenguaje propietario.
LISP
Junto con Fortran, LISP es actualmente uno de los lenguajes de
programacin ms antiguos que se
siguen utilizando comercialmente.
Sus orgenes estn fuertemente ligados con la Inteligencia Articial
y fue pionero de ideas fundamentales en el mbito de la computacin,
como los rboles, la gestin automtica del almacenamiento o el tipado dinmico.
[241]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Elementos de un hilo
Un hilo est compuesto por un identicador nico de hilo, un contador
de programa, un conjunto de registros y una pila.
7.5.
7.5.1.
El concepto de hilo
Los sistemas operativos modernos se basan en el principio de multiprogramacin, es decir, en la posibilidad de manejar distintos hilos de ejecucin de manera
simultnea con el objetivo de paralelizar el cdigo e incrementar el rendimiento de la
aplicacin.
C7
[242]
Esta idea tambin se plasma a nivel de lenguaje de programacin. Algunos ejemplos representativos son las APIs de las bibliotecas de hilos Pthread, Win32 o Java.
Incluso existen bibliotecas de gestin de hilos que se enmarcan en capas software situadas sobre la capa del sistema operativo, con el objetivo de independizar el modelo
de programacin del propio sistema operativo subyacente.
Este ltimo planteamiento es uno de los ms utilizados en el desarrollo de videojuegos con el objetivo de evitar posibles problemas a la hora de portarlos a diversas
plataformas. Recuerde que en la seccin 1.2.3, donde se discuti la arquitectura general de un motor de juegos, se justicaba la necesidad de incluir una capa independiente de la plataforma con el objetivo de garantizar la portabilidad del motor.
Evidentemente, la inclusin de una nueva capa software degrada el rendimiento nal
de la aplicacin.
Es importante considerar que, a diferencia de un proceso, los hilos que pertenecen
a un mismo proceso comparten la seccin de cdigo, la seccin de datos y otros recursos proporcionados por el sistema operativo (ver gura 7.12), como los manejadores
de los archivos abiertos. Precisamente, esta diferencia con respecto a un proceso es lo
que supone su principal ventaja a la hora de utilizar un elemento u otro.
Informalmente, un hilo se puede denir como un proceso ligero que tiene la misma
funcionalidad que un proceso pesado, es decir, los mismo estados. Por ejemplo, si un
hilo abre un chero, ste estar disponible para el resto de hilos de una tarea. Las
ventajas de la programacin multihilo se pueden resumir en las tres siguientes:
Capacidad de respuesta, ya que el uso de mltiples hilos proporciona un enfoque muy exible. As, es posible que un hilo se encuentra atendiendo una
peticin de E/S mientras otro contina con la ejecucin de otra funcionalidad
distinta. Adems, es posible plantear un esquema basado en el paralelismo no
bloqueante en llamadas al sistema, es decir, un esquema basado en el bloqueo
de un hilo a nivel individual.
Estados de un hilo
Un hilo mantiene los mismos estados internos que un proceso: nuevo,
ejecucin, espera, preparado y terminado.
7.5.2.
El segmento de cdigo en el que un proceso puede modicar variables compartidas con otros procesos se denomina seccin crtica. Un ejemplo tpico en el mbito
de la orientacin a objetos son las variables miembro de una clase, suponiendo que
las instancias de la misma pueden atender mltiples peticiones de manera simultnea
mediante distintos hilos de control.
Para evitar inconsistencias, una de las ideas que se plantean es que cuando un
hilo est ejecutando su seccin crtica, ningn otro hilo puede acceder a dicha seccin
crtica. As, si un objeto puede manera mltiples peticiones, no debera ser posible que
un cliente modique el estado de dicha instancia mientras otro intenta leerlo.
Condicin de carrera
Si no se protege adecuadamente la
seccin crtica, distintas ejecuciones de un mismo cdigo pueden generar distintos resultados, generando as una condicin de carrera.
cdigo
datos
archivos
cdigo
pila
registros
pila
datos
registros
pila
archivos
registros
pila
C7
registros
[243]
hilo
(a)
(b)
Figura 7.12: Esquema grco de los modelos de programacin monohilo (a) y multihilo (b).
SECCIN_ENTRADA
SECCIN_CRTICA
SECCIN_SALIDA
SECCIN_RESTANTE
Figura 7.13: Estructura general del cdigo vinculado a la seccin crtica.
[244]
Nombre
Denido por
Documentacin
Lenguajes
Plataformas
Destacado
Descargas
7.6.
La biblioteca de hilos de I CE
7.6.1.
I CE (Internet Communication Engine) es un middleware de comunicaciones orientado a objetos, es decir, I CE proporciona herramientas, APIs, y soporte de bibliotecas
para construir aplicaciones distribuidas cliente-servidor orientadas a objetos (ver gura 7.14).
Una aplicacin I CE se puede usar en entornos heterogneos. Los clientes y los servidores pueden escribirse en diferentes lenguajes de programacin, pueden ejecutarse
en distintos sistemas operativos y en distintas arquitecturas, y pueden comunicarse
empleando diferentes tecnologas de red. La tabla 7.2 resume las principales caractersticas de I CE.
Los principales objetivos de diseo de I CE son los siguientes:
Middleware listo para usarse en sistemas heterogneos.
Proveee un conjunto completo de caractersticas que soporten el desarrollo de
aplicaciones distribuidas reales en un amplio rango de dominios.
Es fcil de aprender y de usar.
Proporciona una implementacin eciente en ancho de banda, uso de memoria
y CPU.
Implementacin basada en la seguridad.
Para instalar I CE en sistemas operativos Debian GNU/Linux, ejecute los siguientes
comandos:
$ sudo apt-get update
$ sudo apt-get install zeroc-ice35
[245]
Aplicacin servidora
Esqueleto
Proxy
API ICE
Adaptador de objetos
API ICE
Red de
comunicaciones
Figura 7.14: Arquitectura general de una aplicacin distribuida desarrollada con el middleware ZeroC I CE.
7.6.2.
Manejo de hilos
En este contexto, I CE proporciona una abstraccin de hilo muy sencilla que posibilita el desarrollo de aplicaciones multihilo altamente portables e independientes
de la plataforma de hilos nativa. Esta abstraccin est representada por la clase IceUtil::Thread.
Como se puede apreciar en el siguiente listado de cdigo, Thread es una clase
abstracta con una funcin virtual pura denominada run(). El desarrollador ha de implementar esta funcin para poder crear un hilo, de manera que run() se convierta en
el punto de inicio de la ejecucin de dicho hilo. Note que no es posible arrojar excepciones desde esta funcin. El ncleo de ejecucin de I CE instala un manejador de
excepciones que llama a la funcin ::std::terminate() si se arroja alguna excepcin.
El resto de funciones de Thread son las siguientes:
start(), cuya responsabilidad es arrancar al hilo y llamar a la funcin run(). Es
posible especicar el tamao en bytes de la pila del nuevo hilo, as como un valor
de prioridad. El valor de retorno de start() es un objeto de tipo ThreadControl,
el cual se discutir ms adelante.
getThreadControl(), que devuelve el objeto de la clase ThreadControl asociado al hilo.
C7
Aplicacin cliente
[246]
id(), que devuelve el identicador nico asociado al hilo. Este valor depender
del soporte de hilos nativo (por ejemplo, POSIX pthreads).
isAlive(), que permite conocer si un hilo est en ejecucin, es decir, si ya se
llam a start() y la funcin run() no termin de ejecutarse.
La sobrecarga de los operadores de comparacin permiten hacer uso de hilos
en contenedores de STL que mantienen relaciones de orden.
Para mostrar cmo llevar a cabo la implementacin de un hilo especco a partir de la biblioteca de I CE se usar como ejemplo uno de los problemas clsicos de
sincronizacin: el problema de los lsofos comensales.
Bsicamente, los lsofos se encuentran comiendo o pensando. Todos comparten
una mesa redonda con cinco sillas, una para cada lsofo. Cada lsofo tiene un plato
individual con arroz y en la mesa slo hay cinco palillos, de manera que cada lsofo
tiene un palillo a su izquierda y otro a su derecha.
Listado 7.18: La clase FilosofoThread
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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:
[247]
Sincronizacin
El problema de los lsofos comensales es uno de los problemas clsicos de sincronizacin y existen muchas modicaciones del mismo que
permiten asimilar mejor los conceptos tericos de la programacin
concurrente.
void
FilosofoThread::run ()
{
while (true) {
coger_palillos();
comer();
dejar_palillos();
pensar();
}
}
7.6.3.
Locking and unlocking
Tpicamente, las operaciones para
adquirir y liberar un cerrojo se denominan lock() y unlock(), respectivamente. La metfora de cerrojo
representa perfectamente la adquisicin y liberacin de recursos compartidos.
C7
21
void coger_palillos ();
22
void dejar_palillos ();
23
void comer () const;
24
void pensar () const;
25
26
int _id;
27
Palillo *_pIzq, *_pDer;
28 };
29
30 #endif
[248]
#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;
I CE proporciona la clase Mutex para modelar esta problemtica de una forma sencilla y directa.
Las funciones miembro ms importantes de esta clase son las que permiten adquirir y liberar el cerrojo:
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.
[249]
Es importante considerar que la clase Mutex proporciona un mecanismo de exclusin mutua bsico y no recursivo, es decir, no se debe llamar a lock() ms de una
vez desde un hilo, ya que esto provocar un comportamiento inesperado. Del mismo
modo, no se debera llamar a unlock() a menos que un hilo haya adquirido previamente el cerrojo mediante lock(). En la siguiente seccin se estudiar otro mecanismo de
sincronizacin que mantiene una semntica recursiva.
La clase Mutex se puede utilizar para gestionar el acceso concurrente a los palillos.
En la solucin planteada a continuacin, un palillo es simplemente una especializacin
de IceUtil::Mutex con el objetivo de incrementar la semntica de dicha solucin.
Riesgo de interbloqueo
Si todos los lsofos 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 lsofo coja ambos palillos.
#ifndef __PALILLO__
#define __PALILLO__
#include <IceUtil/Mutex.h>
class Palillo : public IceUtil::Mutex {
};
#endif
Para que los lsofos 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 exible debido a que los lsofos 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.
C7
11
typedef LockT<Mutex> Lock;
12
typedef TryLockT<Mutex> TryLock;
13 };
[250]
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.
#include <IceUtil/Mutex.h>
class Test {
public:
void mi_funcion () {
_mutex.lock();
for (int i = 0; i < 5; i++)
if (i == 3) return; // Generar un problema...
_mutex.unlock();
}
private:
IceUtil::Mutex _mutex;
};
int main (int argc, char *argv[]) {
Test t;
t.mi_funcion();
return 0;
}
En la lnea 8 , la sentencia return implica abandonar
mi_funcion() sin haber liberado el cerrojo previamente adquirido en la lnea 6 . En este contexto, es bastante
probable que se genere una potencial situacin de interbloqueo que paralizara la ejecucin del programa. La generacin de excepciones no controladas representa otro
caso representativo de esta problemtica.
Para evitar este tipo de problemas, la clase Mutex proporciona las deniciones
de tipo Lock y TryLock, que representan plantillas muy sencillas compuestas de un
constructor en el que se llama a lock() y tryLock(), respectivamente. En el destructor se
llama a unlock() si el cerrojo fue previamente adquirido cuando la plantilla quede fuera
de mbito. En el ejemplo anterior, sera posible garantizar la liberacin del cerrojo al
ejecutar return, ya que quedara fuera del alcance de la funcin. El siguiente listado
de cdigo muestra la modicacin realizada para evitar interbloqueos.
Listado 7.25: Evitando interbloqueos con Lock
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;
};
[251]
7.6.4.
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.
7.6.5.
Introduciendo monitores
C7
[252]
Datos
compartidos
Condiciones
x
y
Operaciones
Cdigo
inicializacin
Figura 7.17: Representacin grca del concepto de monitor.
No olvides...
Comprobar la condicin asociada al
uso de wait siempre que se retorne
de una llamada a la misma.
[253]
timedWait(), que suspende al hilo que invoca a la funcin hasta un tiempo especicado por parmetro. Si otro hilo invoca a notify() o notifyAll() despertando
al hilo suspendido antes de que el timeout expire, la funcin devuelve true y el
hilo suspendido resume su ejecucin con el monitor cerrado. En otro caso, es
decir, si el timeout expira, timedWait() devuelve false.
notify(), que despierta a un nico hilo suspendido debido a una invocacin sobre wait() o timedWait(). Si no existiera ningn hilo suspendido, entonces la
invocacin sobre notify() se pierde. Llevar a cabo una noticacin no implica
que otro hilo reanude su ejecucin inmediatamente. En realidad, esto ocurrira
cuando el hilo que invoca a wait() o timedWait() libera el monitor.
notifyAll(), que despierta a todos los hilos suspendidos por wait() o timedWait().
El resto del comportamiento derivado de invocar a esta funcin es idntico a
notify().
Ejemplo de uso de monitores
Imagine que desea modelar una situacin en un videojuego en la que se controlen
los recursos disponibles para acometer una determinada tarea. Estos recursos se pueden manipular desde el punto de vista de la generacin o produccin y desde el punto
de vista de la destruccin o consumicin. En otras palabras, el problema clsico del
productor/consumidor.
Por ejemplo, imagine un juego de accin de tercera persona en el que el personaje
es capaz de acumular slots para desplegar algn tipo de poder especial. Inicialmente,
el personaje tiene los slots vacos pero, conforme el juego evoluciona, dichos slots
se pueden rellenar atendiendo a varios criterios independientes. Por ejemplo, si el
jugador acumula una cantidad determinada de puntos, entonces obtendra un slot. Si
el personaje principal vence a un enemigo con un alto nivel, entonces obtendra otro
slot. Por otra parte, el jugador podra hacer uso de estas habilidades especiales cuando
as lo considere, siempre y cuando tenga al menos un slot relleno.
Este supuesto plantea la problemtica de sincronizar el acceso concurrente a dichos slots, tanto para su consumo como para su generacin. Adems, hay que tener en
cuenta que slo ser posible consumir un slot cuando haya al menos uno disponible.
Es decir, la problemtica discutida tambin plantea ciertas restricciones o condiciones
que se han de satisfacer para que el jugador pueda lanzar una habilidad especial.
En este contexto, el uso de los monitores proporciona gran exibilidad para modelar una solucin a este supuesto. El siguiente listado de cdigo muestra una posible
implementacin de la estructura de datos que podra dar soporte a la solucin planteada, haciendo uso de los monitores de la biblioteca de hilos de I CE.
Figura 7.18: En los juegos arcade de aviones, los poderes especiales se suelen representar con cohetes para denotar la potencia de los
mismos.
C7
[254]
: Consumidor
: Queue
: Productor
get ()
wait ()
notify
put ()
return item
#include <IceUtil/Monitor.h>
#include <IceUtil/Mutex.h>
#include <deque>
using namespace std;
template<class T>
class Queue : public IceUtil::Monitor<IceUtil::Mutex> {
public:
void put (const T& item) { // Aade un nuevo item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
_queue.push_back(item);
notify();
}
T get () { // Consume un item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
while (_queue.size() == 0)
wait();
T item = _queue.front();
_queue.pop_front();
return item;
}
private:
deque<T> _queue; // Cola de doble entrada.
};
2. Si la estructura
elementos, entonces el hilo se suspende mediante
no contiene
[255]
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.
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 noticacin se pierde. Aunque este hecho no conlleva ningn efecto no deseado, puede generar una reduccin
del rendimiento si el nmero de noticaciones se dispara.
Informacin adicional
Una vez el ms, el uso de alguna
estructura de datos adicional puede
facilitar el diseo de una solucin.
Este tipo de planteamientos puede
incrementar la eciencia de la solucin planteada aunque haya una
mnima sobrecarga debido al uso y
procesamiento de datos extra.
C7
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() naliza, es decir, justo despus de ejecutar la operacin
notify() en este caso particular.
[256]
Por otra parte, los hilos consumidores, es decir, los que invoquen a la funcin get()
incrementan dicha variable antes de realizar un wait(), decrementndola cuando se
despierten.
Note que el acceso a la variable miembro _consumidoresEsperando es exclusivo
y se garantiza gracias a la adquisicin del monitor justo al ejecutar la operacin de
generacin o consumo de informacin por parte de algn hilo.
Listado 7.30: Utilizando monitores (II)
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 <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;
};
7.7.
Concurrencia en C++11
Una de las principales mejoras aportadas por C++11 es el soporte a la programacin concurrente. De hecho, la creacin de hilos resulta trivial, gracias a la clase
Thread8 , y sigue la misma losofa que en otras bibliotecas ms tradicionales (como
pthread):
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
8 http://es.cppreference.com/w/cpp/thread
[257]
int contador = 0;
mutex contador_mutex;
void anyadir_doble (int x) {
int tmp = 2 * x;
contador_mutex.lock();
contador += tmp;
contador_mutex.unlock();
}
#include <atomic>
atomic<int> contador(0);
void anyadir_doble (int x) {
contador += 2 * x;
}
9 http://es.cppreference.com/w/cpp/thread/mutex
C7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[258]
7.7.1.
En esta seccin se discutir una posible implementacin del problema de los lsofos comensales utilizando la funcionalidad proporcionada, desde el punto de vista
de la programacin concurrente, de C++11. As mismo, tambin se introducir el uso
de algunas de las mejoras que incluye esta nueva versin del estndar, las cuales se
discutirn con mayor profundidad en el mdulo 3, Tcnicas Avanzadas de Desarrollo.
El siguiente listado de cdigo muestra la implementacin de la clase Palillo. A diferencia del esquema planteado en la seccin 7.6.3, esta implementacin se basa en la
composicin en lugar de herencia con el objetivo de discutir las diferencias existentes
entre ambos enfoques a la hora de manipular mutex. En principio, la primera opcin
a considerar debera ser la composicin, es decir, un esquema basado en incluir una
variable miembro de tipo mutex dentro de la declaracin de la clase asociada al recurso compartido. Recuerde que las relaciones de herencia implican fuertes restricciones
desde el punto de vista del diseo y se deberan establecer cuidadosamente.
Listado 7.34: Filsofos comensales en C++11 (Palillo)
1 class Palillo {
2 public:
3
mutex _mutex;
4 };
nica
se utiliza el wrapper 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 lsofo ha adquirido los palillos.
Una vez denida la funcionalidad general asociada a un lsofo, el siguiente paso
consiste en instanciar los palillos a los propios lsofos.
Listado 7.35: Filsofos comensales en C++11 (Funcin comer)
1 int main () {
2
/* Funcin lambda (funcin annima) */
3
/* Adis a los punteros a funciones con auto */
4
auto comer = [](Palillo* pIzquierdo, Palillo* pDerecho,
5
int id_filosofo, int id_pIzquierdo, int id_pDerecho) {
6
/* Para evitar interbloqueos */
7
lock(pIzquierdo->_mutex, pDerecho->_mutex);
8
9
/* Wrapper para adquirir el mutex en un bloque de cdigo */
10
/* Indica que el mutex ha sido adquirido y que debe
11
adoptar al propietario del cierre */
12
lock_guard<mutex> izquierdo(pIzquierdo->_mutex, adopt_lock);
13
string si = "\tFilsofo " + to_string(id_filosofo) +
[259]
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
La ltima parte del cdigo, en las lneas 32-34 , lleva a cabo la ejecucin efectiva de los hilos y la espera a la nalizacin de los mismos mediante la funcin thread::join().
Listado 7.37: Filsofos comensales en C++11 (Creacin lsofos)
1 /* Vector de filsofos */
C7
[260]
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<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));
7.8.
Multi-threading en Ogre3D
Desde un punto de vista general, la posible integracin de hilos de ejecucin adicionales con respecto al ncleo de ejecucin de Ogre se suele plantear con aspectos
diferentes al meramente grco. De hecho, es poco comn que el proceso de rendering se delegue en un segundo plano debido a la propia naturaleza asncrona de la
GPU (Graphic Processing Unit). Por lo tanto, no existe una necesidad real de utilizar algn esquema de sincronizacin para evitar condiciones de carrera o asegurar la
exclusin mutua en determinadas partes del cdigo.
Sin embargo, s que es posible delegar en un segundo plano aspectos tradicionalmente independientes de la parte grca, como por ejemplo el mdulo de Inteligencia
Articial 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 ejemplicar este problema, a continuacin se detallan las instrucciones para compilar el cdigo fuente de Ogre 1.8.110 .
Antes de nada, es necesario asegurarse de que la biblioteca de hilos de boost est
instalada y es recomendable utilizar cmake para generar el Makele 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
10 http://www.ogre3d.org/download/source
[261]
A continuacin, es necesario descomprimir el cdigo de Ogre y crear un directorio
en el que se generar el resultado de la compilacin:
El siguiente paso consiste en ejecutar la interfaz grca 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, especicar 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 gura 7.21. Despus, hay que realizar las siguientes acciones:
1. Hacer click en el botn Congure.
2. Ajustar el parmetro OGRE_CONFIG_THREADS11 .
3. Hacer click de nuevo en el botn Congure.
4. Hacer click en el botn Generate para generar el archivo Makele.
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
12 Segn los desarrolladores de Ogre, slo se recomienda utilizar un valor de 0 o de 2 en sistemas GNU/Linux
13 http://www.ogre3d.org/docs/api/html/classOgre_1_1Resource_1_
1Listener.html
C7
[262]
Figura 7.21: Generando el chero Makele mediante cmake para compilar Ogre 1.8.1.
3 (const Ogre::String& name, const Ogre::String& group)
4 {
5
// Obtencin del recurso por nombre...
6
TrackPtr trackPtr = getByName(name);
7
8
// Si no ha sido creado, se crea.
9
if (trackPtr.isNull())
10
trackPtr = create(name, group);
11
12
// Carga en segundo plano y listener.
13
trackPtr->setBackgroundLoaded(true);
14
trackPtr->addListener(new MyListener);
15
16
// Escala la carga del recurso en segundo plano.
17
if (trackPtr->isBackgroundLoaded())
18
trackPtr->escalateLoading();
19
else
20
trackPtr->load();
21
22
return trackPtr;
23 }
Otra posible variacin en el diseo podra haber consistido en delegar la noticacin de la funcin de retrollamada a la propia clase TrackPtr, es decir, que esta clase
heredase de Ogre::Resource::Listener.
#ifndef __MYLISTENERH__
#define __MYLISTENERH__
#include <OGRE/OgreResource.h>
#include <iostream>
using namespace std;
class MyListener : public Ogre::Resource::Listener {
void backgroundLoadingComplete (Ogre::Resource* r) {
cout << "Carga en segundo plano completada..." << endl;
}
};
#endif
7.9.
Updating threads...
Idealmente, el mdulo de IA debera aprovechar los recursos disponibles, no utilizados por el motor de
rendering, para actualizar su estado. Adems, se debera contemplar
la posibilidad de sincronizacin entre los mismos.
[264]
En la solucin planteada se ha optado por utilizar el mecanismo de exclusin mutua proporcionado por I CE para sincronizar el acceso concurrente al estado de AIThread con un doble propsito: i) ilustrar de nuevo su utilizacin en otro ejemplo y ii)
recordar que es posible que distintos hilos de control interacten con el mdulo de IA.
Note cmo en la lnea 23 se ha denido un manejador para las instancias de
la clase AIThread utilizando IceUtil::Handle con el objetivo de gestionar de manera
inteligente dichas instancias. Recuerde que la funcin miembro run() especicar el
comportamiento del hilo y es necesario implementarla debido al contracto funcional
heredado de la clase IceUtil::Thread.
Listado 7.40: Clase AIThread
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
#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
Actualmente, las responsabilidades del hilo encargado de procesar la IA del juego Simon son dos. Por una parte, actualizar peridicamente la secuencia de colores
a repetir por el usuario. Esta actualizacin se llevara a cabo en la funcin run(), pudiendo ser ms o menos sosticada en funcin del nivel de IA a implementar. Por otra
parte, facilitar la obtencin del siguiente color de la secuencia cuando el jugador haya
completado la subsecuencia anterior.
El siguiente listado de cdigo muestra una posible implementacin de dichas funciones. Bsicamente, el hilo de IA genera los colores de manera aleatoria, pero sera
posible realizar una implementacin ms sosticada para, por ejemplo, modicar o
invertir de algn modo la secuencia de colores que se va generando con el paso del
tiempo.
Considere que el delay de tiempo existente entre actualizacin y actualizacin del
mdulo de IA debera estar condicionado por la actual tasa de frames por segundo
del juego. En otras palabras, la tasa de actualizacin del mdulo de IA ser ms o
menos exigente en funcin de los recursos disponibles con el objetivo de no penalizar
el rendimiento global del juego.
Uso de hilos
Considere el uso de hilos cuando
realmente vaya a mejorar el rendimiento de su aplicacin. Si desde
el hilo de control principal se puede atender la lgica de IA, entonces
no sera necesario delegarla en hilos
adicionales.
[266]
: MyApp
<<create ()>>
: AIThread
start ()
update ()
Figura 7.23: Diagrama de interaccin entre la clase principal del juego y AIThread.
7
8
9
10
11
12
13
14
15 }
}
_AI = new AIThread(IceUtil::Time::seconds(1));
IceUtil::ThreadControl tc = _AI->start();
// Se desliga del hilo principal.
tc.detach();
// ...
Programacin Grfica
El objetivo de este mdulo titulado Programacin Grfica del Cur
so de Experto en Desarrollo de Videojuegos es cubrir los aspectos
esenciales relativos al desarrollo de un motor grfico interactivo. En
este contexto, el presente mdulo cubre aspectos esenciales y bsicos
relativos a los fundamentos del desarrollo de la parte grfica, como
por ejemplo el pipeline grfico, como elemento fundamental de la
arquitectura de un motor de juegos, las bases matemticas, las APIs
de programacin grfica, el uso de materiales y texturas, la
iluminacin o los sistemas de partculas. As mismo, el presente
mdulo tambin discute aspectos relativos a la exportacin e
importacin de datos, haciendo especial hincapi en los formatos
existentes para tratar con informacin multimedia. Finalmente, se
pone de manifiesto la importancia del uso de elementos directamente
conectados con el motor grfico, como la simulacin fsica, con el
objetivo de dotar de ms realismo en el comportamiento de los
objetos y actores que intervienen en el juego.
Captulo
Fundamentos de Grcos
Tridimensionales
Carlos Gonzlez Morcillo
8.1.
An ms rpido!!
Si cada frame tarda en desplegarse
ms de 40ms, no conseguiremos el
mnimo de 25 fps (frames per second) establecido como estndar en
cine. En videojuegos, la frecuencia
recomendable mnima es de unos
50 fps.
Introduccin
Desde el punto de vista del usuario, un videojuego puede denirse como una aplicacin software que responde a una serie de eventos, redibujando la escena y generando una serie de respuestas adicionales (sonido en los altavoces, vibraciones en
dispositivos de control, etc...).
El redibujado de esta escena debe realizarse lo ms rpidamente posible. La capa
de aplicacin en el despliegue grco es una de las actividades que ms ciclos de CPU
consumen (incluso, como veremos en la seccin 8.3, con el apoyo de las modernas
GPUs Graphics Processing Unit existentes en el mercado).
269
[270]
Uno de los errores habituales en videojuegos amateur es la falta de comunicacin clara entre programadores y diseadores. El desarrollador debe especicar claramente las capacidades soportadas por el motor grco al equipo
de modeladores, animadores y equipo artstico en general. Esta informacin
debe comprender tanto las tcnicas de despliegue soportadas, como el nmero
de polgonos disponibles para el personaje principal, enemigos, fondos, etc...
A un alto nivel de abstraccin podemos ver el proceso de render como el encargado de convertir la descripcin de una escena tridimensional en una imagen bidimensional. En los primeros aos de estudio de esta disciplina, la investigacin se centr en
cmo resolver problemas relacionados con la deteccin de supercies visibles, sombreado bsico, etc. Segn se encontraban soluciones a estos problemas, se continu el
estudio de algoritmos ms precisos que simularan el comportamiento de la luz de una
forma ms precisa.
En esencia, el proceso de rendering de una escena 3D requiere los siguientes elementos:
Supercies. La geometra de los objetos que forman la escena debe ser denida
empleando alguna representacin matemtica, para su posterior procesamiento
por parte del ordenador.
Cmara. La situacin del visor debe ser denida mediante un par (posicin,
rotacin) en el espacio 3D. El plano de imagen de esta cmara virtual denir
el resultado del proceso de rendering. Como se muestra en la Figura 8.1, para
imgenes generadas en perspectiva, el volumen de visualizacin dene una pirmide truncada que selecciona los objetos que sern representados en la escena.
Esta pirmide se denomina Frustum.
Fuentes de luz. Las fuentes de luz emiten rayos que interactan con las supercies e impactarn en el plano de imagen. Dependiendo del modo de simulacin
de estos impactos de luz (de la resolucin de la denominada ecuacin de render), tendremos diferentes mtodos de rendering.
Propiedades de las supercies. En este apartado se incluyen las propiedades
de materiales y texturas que describen el modelo de rebote de los fotones sobre
las supercies.
Uno de los principales objetivos en sntesis de imagen es el realismo. En general, segn el mtodo empleado para la resolucin de la ecuacin de render tendremos
diferentes niveles de realismo (y diferentes tiempos de cmputo asociados). El principal problema en grcos en tiempo real es que las imgenes deben ser generadas
muy rpidamente. Eso signica, como hemos visto anteriormente, que el motor grco dispone de menos de 40 ms para generar cada imagen. Habitualmente este tiempo
es incluso menor, ya que es necesario reservar tiempo de CPU para otras tareas como
el clculo de la Inteligencia Articial, simulacin fsica, sonido...
Tiempo de cmputo
En sntesis de imagen realista (como por ejemplo, las tcnicas de rendering empleadas en pelculas de
animacin) el clculo de un solo fotograma de la animacin puede requerir desde varias horas hasta das,
empleando computadores de altas
prestaciones.
8.1. Introduccin
[271]
Lista de
despliegue
Pipeline
Raster
Plano de
Imagen
Punto
de
Vista
C8
Punto
de
Vista
Plano de
Imagen
RayCasting
Mapeado Inverso
Figura 8.2: Diferencias entre las tcnicas de despliegue interactivas (mtodos basados en ScanLine) y
realistas (mtodos basados en RayCasting). En el Pipeline Hardware, la seleccin del pxel ms cercano
relativo a cada tringulo se realiza directamente en Hardware, empleando informacin de los fragmentos
(ver Seccin 8.2).
En 1968 Arthur Appel describi el primer mtodo para generar imgenes por
computador lanzando rayos desde el punto de vista del observador. En este trabajo,
generaba la imagen resultado en un plotter donde dibujaba punto a punto el resultado del proceso de render. La idea general del mtodo de RayCasting es lanzar rayos
desde el plano de imagen, uno por cada pxel, y encontrar el punto de interseccin
ms cercano con los objetos de la escena. La principal ventaja de este mtodo frente a
los mtodos de tipo scanline que emplean zbuffer es que es posible generar de forma
consistente la imagen que represente el mundo 3D, ya que cualquier objeto que pueda
ser descrito mediante una ecuacin puede ser representado de forma correcta mediante
RayCasting.
Como puede verse en la Figura 8.2, existen diferencias importantes entre el mtodo de despliegue que implementan las tarjetas aceleradoras 3D (y en general los motores de visualizacin para aplicaciones interactivas) y el mtodo de RayCasting. El
pipeline grco de aplicaciones interactivas (como veremos en la seccin 8.2) puede
describirse de forma general como el que, a partir de una lista de objetos geomtricos
a representar y, tras aplicar la serie de transformaciones geomtricas sobre los objetos,
la vista y la perspectiva, obtienen una imagen raster dependiente del dispositivo de visualizacin. En este enfoque, las primitivas se ordenan segn la posicin de la cmara
y slo las visibles sern dibujadas. Por el contrario, en mtodos de sntesis de imagen
realista (como la aproximacin inicial de RayCasting) calcula los rayos que pasan por
cada pxel de la imagen y recorre la lista de objetos, calculando la interseccin (si
hay alguna) con el objeto ms cercano. Una vez obtenido el punto de interseccin, se
evala (empleando un modelo de iluminacin) el valor de sombreado correspondiente
a ese pxel.
[272]
8.2.
El Pipeline Grco
El Pipeline est dividido en etapas funcionales. Al igual que ocurre en los pipeline de fabricacin industrial, algunas de estas etapas se realizan en paralelo y otras
secuencialmente. Idealmente, si dividimos un proceso en n etapas se incrementar la
velocidad del proceso en ese factor n. As, la velocidad de la cadena viene determinada
por el tiempo requerido por la etapa ms lenta.
Como seala Akenine-Mler [8], el pipeline interactivo se divide en tres etapas
conceptuales de Aplicacin, Geometra y Rasterizacin (ver Figura 8.3). A continuacin estudiaremos estas etapas.
Etapa de Aplicacin
8.2.2.
Transf Modelado
Coord. Universales
1 En el mundo fsico, la pelcula en antiguas cmaras analgicas, o el sensor de imagen de las cmaras
digitales.
Coord. Visualizacin
Vertex Shader
Transf Proyeccin
Coord. Normalizadas
Coord. Recortadas
Transf Pantalla
Coord. Pantalla
Etapa de Geometra
Transf Visualizacin
Transf Recorte
Rasterizacin
8.2.1.
Aplicacin
Coord. Modelo (Locales)
Etapa de Geometra
La Figura 8.3 muestra los pasos generales del Pipeline asociado a la transformacin de una escena 3D hasta su representacin nal en el dispositivo de visualizacin
(tpicamente una pantalla con una determinada resolucin).
App
Config. Tringulo
Recorrido Tring.
Pixel Shader
Fusin (Merging)
[273]
Sistema de
Coordenadas Local
(Objeto)
Z
X
Z
Y
X
Sistema de
Coordenadas de
Visualizacin
Y
C8
Sistema de
Coordenadas
Universal (SRU)
Plano de
visualizacin
Figura 8.4: Sistema de coordenadas de visualizacin y su relacin con otros sistemas de coordenadas de la
escena.
[274]
X
Z
Y
X
Transformacin
de Visualizacin
Transformacin
de Visualizacin
Z
Figura 8.5: El usuario especica la posicin de la cmara (izquierda) que se transforma, junto con los
objetos de la escena, para posicionarlos a partir del origen del SRU y mirando en la direccin negativa del
eje Z. El rea sombreada de la cmara se corresponde con el volumen de visualizacin de la misma (slo
los objetos que estn contenidos en esa pirmide sern nalmente representados).
[275]
Vrtices
Nuevos
X
Z
Figura 8.6: Los objetos que intersecan con los lmites del cubo unitario (arriba) son recortados, aadiendo nuevos vrtices. Los objetos
que estn totalmente dentro del cubo unitario se pasan directamente a
la siguiente etapa. Los objetos que
estn totalmente fuera del cubo unitario son descartados.
8.2.3.
Etapa Rasterizacin
C8
nicamente los objetos que estn dentro del volumen de visualizacin deben ser
generados en la imagen nal. Los objetos que estn totalmente dentro del volumen de
visualizacin sern copiados ntegramente a la siguiente etapa del pipeline. Sin embargo, aquellos que estn parcialmente incluidas necesitan ser recortadas, generando
nuevos vrtices en el lmite del recorte. Esta operacin de Transformacin de Recorte se realiza automticamente por el hardware de la tarjeta grca. En la Figura 8.6 se
muestra un ejemplo simplicado de recorte.
[276]
8.2.4.
Proyeccin en Perspectiva
En grcos por computador es posible elegir entre diferentes modelos de proyeccin de los objetos sobre el plano de visualizacin. Un modo muy utilizado en aplicaciones de CAD es la proyeccin de los objetos empleando lneas paralelas sobre el
plano de proyeccin, mediante la denominada proyeccin paralela. En este modo de
proyeccin se conservan las proporciones relativas entre objetos, independientemente
de su distancia.
Mediante la proyeccin en perspectiva se proyectan los puntos hasta el plano
de visualizacin empleando trayectorias convergentes en un punto. Esto hace que los
objetos situados ms distantes del plano de visualizacin aparezcan ms pequeos en
la imagen. Las escenas generadas utilizando este modelo de proyeccin son ms realistas, ya que sta es la manera en que el ojo humano y las cmaras fsicas forman
imgenes.
En la proyeccin en perspectiva, las lneas paralelas convergen en un punto, de
forma que los objetos ms cercanos se muestran de un tamao mayor que los lejanos.
Desde el 500aC, los griegos estudiaron el fenmeno que ocurra cuando la luz pasaba
a travs de pequeas aberturas. La primera descripcin de una cmara estenopeica se
atribuye al atrnomo y matemtico holands Gemma Frisius que en 1545 public la
primera descripcin de una cmara oscura en la observacin de un eclipse solar (ver
Figura 8.8). En las cmaras esteneopeicas la luz pasa a travs de un pequeo agujero
para formar la imagen en la pelcula fotosensible, que aparece invertida. Para que la
imagen sea ntida, la abertura debe ser muy pequea.
Siguiendo la misma idea y desplazando el plano de proyeccin delante del origen,
tenemos el modelo general proyeccin en perspectiva.
Consideraremos en el resto de la seccin que ya se ha realizado la transformacin
de visualizacin alineando la cmara y los objetos de la escena mirando en direccin
al eje negativo Z, que el eje Y est apuntando hacia arriba y el eje X positivo a la
derecha (como se muestra en la Figura 8.5).
p
Config.
Tringulos
(Tr. Setup)
Recorrido
Tringulos
(Scan Conv.)
Sombreado
de Pxel
(P. Shading)
Figura 8.7: Representacin del resultado de las principales etapas del
Pipeline de Rasterizacin.
Perspectiva
La mayora de los juegos 3D se basan en el uso de cmaras de proyeccin en perspectiva. Estudiaremos
con ms detalle este tipo de modelos de proyeccin en el captulo 9.
pz
p'
Figura 8.8: Descripcin de la primera cmara estenopeica (pinhole camera o camera obscura) por
Gemma Frisius.
p'
P'z = -d
d
d
X
Z
p'x
px
Figura 8.9: Modelo de proyeccin en perspectiva simple. El plano de proyeccin innito est denido en
z = d, de forma que el punto p se proyecta sobre p .
[277]
8.3.
Geometry Shader
Transf. Recorte
Transf. Pantalla
Config. Tringulo
Recorrido Tring.
Pixel Shader
Fusin (Merger)
Figura 8.10: Implementacin tpica del pipeline en GPU con soporte Hbrido (OpenGL 2). La letra asociada a cada etapa indica el
nivel de modicacin permitida al
usuario; P indica totalmente programable, F indica jo (no programable), y C indica congurable pero
no programable.
GK110
Itanium
Poulson
GP
U
GF100
zEC12
2
GT200
CP
U
Vertex Shader
IBM z196
Opteron 2400
G80
AMD K10
Core 2 Duo
Pentium IV
Pentium III
Ao 1999
2001
GF4
Itanium 2
2003
2005
2007
2009
2011
2013
Resulta de especial inters conocer aquellas partes del Pipeline de la GPU que
puede ser programable por el usuario mediante el desarrollo de shaders. Este cdigo
se ejecuta directamente en la GPU, y permite realizar operaciones a diferentes niveles
con alta eciencia.
C8
Empleando tringulos semejantes (ver Figura 8.9 derecha), obtenemos las siguientes coordenadas:
d
d px
px
=
px =
(8.1)
px
pz
pz
[278]
En GPU, las etapas del Pipeline grco (estudiadas en la seccin 8.2) se implementan en un conjunto de etapas diferente, que adems pueden o no ser programables
por parte del usuario (ver Figura 8.10). Por cuestiones de eciencia, algunas partes
del Pipeline en GPU no son programables, aunque se prevee que la tendencia en los
prximos aos sea permitir su modicacin.
La etapa asociada al Vertex Shader es totalmente programable, y encapsula las
cuatro primeras etapas del pipeline grco que estudiamos en la seccin 8.2: Transformacin de Modelado, Transformacin de Visualizacin, Sombreado de Vrtices
(Vertex Shader) y Transformacin de Proyeccin.
La etapa referente al Geometry Shader es otra etapa programable que permite
realizar operaciones sobre las primitivas geomtricas.
El trmino GPU
Desde el 1999, se utiliza el trmino
GPU (Grpahics Processing Unit)
acuado por NVIDIA para diferenciar la primera tarjeta grca que
permita al programador implementar sus propios algoritmos (GeForce
256).
Las etapas de Transformacin de Recorte, Transformacin de Pantalla, Conguracin de Tringulo, Recorrido de Tringulo y Fusin tienen un comportamiento funcional similar al estudiado en la seccin 8.2, por lo que no sern descritas de nuevo.
Finalmente, el Pixel Shader es la ltima etapa totalmente programable del pipeline
en GPU y permite al programador desarrollar operaciones especcas a nivel de pxel.
El desarrollo de shaders se ha incorporado desde el 2009 en las especicaciones de
las pricipales APIs grcas (OpenGL y Direct3D). Estas llamadas son compiladas a un
lenguaje ensamblador intermedio independiente de la tarjeta grca. Son los drivers
de cada tarjeta grca los que transforman este lenguaje intermedio en instrucciones
especcas para cada tarjeta.
Veremos a continuacin brevemente algunas caractersticas de estas etapas programables de la GPU.
8.3.1.
Vertex Shader
Ensamblador?
En realidad este cdigo ensamblador intermedio puede verse como
una especie de cdigo de mquina
virtual que garantiza la compatibilidad entre diferentes dispositivos
hardware.
[279]
8.3.2.
Geometry Shader
La entrada de este mdulo lo forman la especicacin de los objetos con sus vrtices asociados. Para cada primitiva de entrada, el geometry shader devolver cero o
ms primitivas de salida. Las primitivas de entrada y salida no tienen por qu ser del
mismo tipo. Por ejemplo, es posible indicar un tringulo como entrada (tres vrtices
3d) y devolver el centroide (un punto 3D) como salida. Las primitivas del ujo de
salida del geometry shader se obtienen en el mismo orden que se especicaron las
primitivas de entrada.
Este tipo de shaders se emplean para la simulacin de pelo, para encontrar los
bordes de los objetos, o para implementar algunas tcnicas de visualizacin avanzadas
como metabolas o simulacin de telas.
8.3.3.
Pixel Shader
Notacin...
En OpenGL al Pixel Shader se
le denomina Fragment Shader. En
realidad es un mejor nombre, porque se trabaja a nivel de fragmento.
Compatiblidad
En algunos casos, como en el desarrollo de videojuegos para PC, el
programador no puede hacer casi
ninguna suposicin sobre el hardware subyacente en la mquina
donde se ejecutar nalmente el
programa. En el caso de desarrollo
para consolas, los entornos de ejecucin concretos estn mucho ms
controlados.
8.4.
El objetivo de esta seccin es proporcionar una primera visin general sobre los
conceptos generales subyacentes en cualquier motor grco 3D interactivo. Estos conceptos sern estudiados en profundidad a lo largo de este documento, mostrando su
uso prctico mediante ejemplos desarrollados en C++ empleando el motor grco
OGRE.
Como se ha visto en la introduccin del captulo, los videojuegos requieren hacer
un uso eciente de los recursos grcos. Hace dos dcadas, los videojuegos se diseaban especcamente para una plataforma hardware especca, y las optimizaciones
podan realizarse a muy bajo nivel. Actualmente, el desarrollo de un videojuego tiende a realizarse para varias plataformas, por lo que el uso de un motor grco que
nos abstraiga de las particularidades de cada plataforma no es una opcin, sino una
necesidad.
En estos desarrollos multiplataforma es necesario abordar aproximaciones de diseo que permitan emplear diferentes perles de ejecucin. Por ejemplo, en mquinas
con grandes prestaciones se emplearn efectos y tcnicas de despliegue ms realistas,
mientras que en mquinas con recursos limitados se utilizarn algoritmos con menores requisitos computacionales y versiones de los recursos grcos adaptadas (con
diferente nivel de detalle asociado).
Las limitaciones asociadas a los recursos computacionales son una constante en el
rea del desarrollo de videojuegos. Cada plataforma conlleva sus propias limitaciones
y restricciones, que pueden asociarse en las categoras de:
C8
[280]
Figura 8.13: Capturas de algunos videojuegos desarrollados con motores libres. La imagen de la izquierda
se corresponde con Planeshift, realizado con Crystal Space. La captura de la derecha es del videojuego
H-Craft Championship, desarrollado con Irrlicht.
Tiempo de Procesamiento. El desarrollo de videojuegos en prcticamente cualquier plataforma actual require el manejo de mltiples ncleos de procesamiento (tanto de CPU como GPU). El manejo explcito de la concurrencia (habitualmente a nivel de hilos) es necesario para mantener una alta tasa de Frames por
Segundo.
Almacenamiento. En el caso de ciertos dispositivos como consolas, la variedad
de unidades de almacenamiento de los recursos del juego (con velocidades de
acceso y transferencia heterogneas), dicultan el desarrollo del videojuego. En
ciertas plataformas, no se dispone de aproximaciones de memoria virtual, por
lo que el programador debe utilizar explcitamente superposiciones (overlays)
para cargar las zonas del juego que van a emplearse en cada momento.
Dada la gran cantidad de restricciones que deben manejarse, as como el manejo
de la heterogeneidad en trminos software y hardware, es necesario el uso de un motor de despliegue grco para desarrollar videojuegos. A continuacin enunciaremos
algunos de los ms empleados.
8.5.
Casos de Estudio
En esta seccin se estudiarn algunos de los motores grcos 3D libres, comentando brevemente algunas de sus caractersticas ms destacables.
Crystal Space. Crystal Space (http://www.crystalspace3d.org/) es un
framework completo para el desarrollo de videojuegos escrito en C++, desarrollado inicialmente en 1997. Se distribuye bajo licencia libre LGPL, y es multiplataforma (GNU/Linux, Windows y Mac).
Panda 3D. Panda 3D (http://www.panda3d.org/) es un motor para el desarrollo de videojuegos multiplataforma. Inicialmente fue desarrollado por Disney
para la construccin del software asociado a las atracciones en parques temticos, y posteriormente liberado en 2002 bajo licencia BSD. Este motor mul-
[281]
Irrlicht. Este motor grco de renderizado 3D, con primera versin publicada
en el 2003 (http://irrlicht.sourceforge.net/), ofrece interfaces para
C++ y .NET. Existen gran cantidad de wrappers a diferentes lenguajes como
Java, Perl, Python o Lua. Irrlicht tiene una licencia Open Source basada en la
licencia de ZLib. Irrlicht es igualmente multiplataforma (GNU/Linux, Windows
y Mac).
OGRE. OGRE (http://www.ogre3d.org/) es un motor para grcos 3D
libre multiplataforma. Sus caractersticas sern estudiadas en detalle en la seccin 8.6.
De entre los motores estudiados, OGRE 3D ofrece una calidad de diseo superior,
con caractersticas tcnicas avanzadas que han permitido el desarrollo de varios videojuegos comerciales. Adems, el hecho de que se centre exclusivamente en el capa
grca permite utilizar gran variedad de bibliotecas externas (habitualmente accedidas
mediante plugins) para proporcionar funcionalidad adicional. En la siguiente seccin
daremos una primera introduccin y toma de contacto al motor libre OGRE.
8.6.
Introduccin a OGRE
OGRE es un motor orientado a objetos libre para aplicaciones grcas 3D interactivas. El nombre del motor OGRE es un acrnimo de Object-oriented Graphics
Rendering Engine. Como su propio nombre indica, OGRE no es un motor para el
desarrollo de videojuegos; se centra exclusivamente en la denicin de un middleware para el renderizado de grcos 3D en tiempo real.
Figura 8.14: El logotipo de OGRE
3D es un... OGRO!
Slo Rendering!
El desarrollo de OGRE se centra
exclusivamente en la parte de despliegue grco. El motor no proporciona mecanismos para capturar la
interaccin del usuario, ni para reproduccin audio o gestin del estado interno del videojuego.
El proyecto de OGRE comenz en el 2000 con el propsito de crear un motor grco bien diseado. El lder del proyecto Steve Streeting dene el desarrollo de OGRE
como un proyecto basado en la calidad ms que en la cantidad de caractersticas que
soporta, porque la cantidad viene con el tiempo, y la calidad nunca puede aadirse a
posteriori. La popularidad de OGRE se basa en los principios de meritocracia de los
proyectos de software libre. As, el sitio web de OGRE 2 recibe ms de 500.000 visitas
diarias, con ms de 40.000 descargas mensuales.
La ltima versin la biblioteca en desarrollo de la biblioteca (1.9) ha sido publicada en Abril de 2013 3 .
El ncleo principal de desarrolladores en OGRE se mantiene deliberadamente pequeo y est formado por profesionales con dilatada experiencia en proyectos de ingeniera reales.
OGRE tiene una licencia LGPL Lesser GNU Public License. Esta licencia se utiliza con frecuencia en bibliotecas que ofrecen funcionalidad que es similar a la de
otras bibliotecas privativas. Por cuestin de estrategia, se publican bajo licencia LGPL
(o GPL Reducida) para permitir que se enlacen tanto por programas libres como no
libres. La nica restriccin que se aplica es que si el enlazado de las bibliotecas es esttico, la aplicacin resultado debe ser igualmente LGPL (porque el enlazado esttico
tambin enlaza la licencia).
2 http://www.ogre3d.org
3 En
C8
[282]
Portabilidad
Uno de los objetivos de diseo
de OGRE es utilizar otras bibliotecas multiplataforma estables en
su desarrollo, como FreeType para el despliegue de fuentes TrueType, OpenIL para la carga y manipulacin de imgenes y ZLib para
la gestin de archivos comprimidos
ZIP.
SceneGraph
Adjunto
Attached
SceneNode
SceneNode
Adjunto
Attached
MovableObject MovableObject
Implementa
Entity
Entity
Contiene
Subentity
SubEntity
Implementa
Renderable A
Renderable B
[283]
La Figura 8.15 muestra el esquema general de gestin del grafo de escena en
OGRE. Los Renderables manejan la geometra de la escena. Todas las propiedades para el despliegue de estos Renderables (como por ejemplo los materiales)
se gestionan en objetos de tipo Entidad (Entity) que pueden estar formados por
varios objetos SubEntidad (SubEntity). Como se ha comentado anteriormente,
la escena se Compone de nodos de escena. Estos SceneNodes se adjuntan a la
escena. Las propiedades de esos nodos de escena (geometra, materiales, etc...)
se ofrecen al SceneGraph mediante un MovableObject. De forma similar, los
MovableObjects no son subclases de SceneNode, sino que se adjuntan. Esto
permite realizar modicaciones en la implementacin del grafo de escena sin
necesidad de tocar ninguna lnea de cdigo en la implementacin de los objetos
que contiene. Veremos ms detalles sobre el trabajo con el grafo de escena a lo
largo de este mdulo.
Aceleracin Hardware. OGRE necesita una tarjeta aceleradora grca para
poder ejecutarse (con soporte de direct rendering mode). OGRE permite denir
el comportamiento de la parte programable de la GPU mediante la denicin de
Shaders, estando al mismo nivel de otros motores como Unreal o CryEngine.
Optimizacin ofine
El proceso de optimizacin de al
formato binario de OGRE calcula
el orden adecuado de los vrtices
de la malla, calcula las normales de
las caras poligonales, as como versiones de diferentes niveles de detalle de la malla. Este proceso evita el clculo de estos parmetros en
tiempo de ejecucin.
Materiales. Otro aspecto realmente potente en OGRE es la gestin de materiales. Es posible crear materiales sin modicar ni una lnea de cdigo a compilar.
El sistema de scripts para la denicin de materiales de OGRE es uno de los
ms potentes existentes en motores de rendering interactivo. Los materiales de
OGRE se denen mediante una o ms Tcnicas, que se componen a su vez de
una o ms Pasadas de rendering (el ejemplo de la Figura 8.16 utiliza una nica pasada para simular el sombreado a lpiz mediante hatching). OGRE busca
automticamente la mejor tcnica disponible en un material que est soportada
por el hardware de la mquina de forma transparente al programador. Adems,
es posible denir diferentes Esquemas asociados a modos de calidad en el despliegue.
Animacin. OGRE soporta tres tipos de animacin ampliamente utilizados en
la construccin de videojuegos: basada en esqueletos (skeletal), basada en vrtices (morph y pose). En la animacin mediante esqueletos, OGRE permite el
uso de esqueletos con animacin basada en cinemtica directa. Existen multitud de exportadores para los principales paquetes de edicin 3D. En este mdulo
utilizaremos el exportador de Blender.
El sistema de animacin de OGRE se basa en el uso de controladores, que
modican el valor de una propiedad en funcin de otro valor. En el caso de
animaciones se utiliza el tiempo como valor para modicar otras propiedades
(como por ejemplo la posicin del objeto). El motor de OGRE soporta dos modelos bsicos de interpolacin: lineal y basada en splines cbicas.
La animacin y la geometra asociada a los modelos se almacena en un nico
formato binario optimizado. El proceso ms empleado se basa en la exportacin
desde la aplicacin de modelado y animacin 3D a un formato XML (Ogre
XML) para convertirlo posteriormente al formato binario optimizado mediante
la herramienta de lnea de rdenes OgreXMLConverter.
Composicin y Postprocesado. El framework de composicin facilita al programador incluir efectos de postprocesado en tiempo de ejecucin (siendo una
extensin del pixel shader del pipeline estudiado en la seccin 8.3.3). La aproximacin basada en pasadas y diferentes tcnicas es similar a la explicada para
el gestor de materiales.
Plugins. El diseo de OGRE facilita el diseo de Plugins como componentes
que cooperan y se comunican mediante un interfaz conocido. La gestin de
archivos, sistemas de rendering y el sistema de partculas estn implementados
basados en el diseo de Plugins.
C8
[284]
8.6.1.
Arquitectura General
El diagrama de la Figura 8.17 resume algunos de los objetos principales del motor
OGRE. No es un diagrama exhaustivo, pero facilita la comprensin de los principales
mdulos que utilizaremos a lo largo del curso.
Objeto Root
Root
Gestin de Escena
Scene Node
SceneManager
Material
MovableObject
Plugin
OctreeSceneManager
Plugin
Entity
Camera
CustomMovable
Light
Gestin de Recursos
ResourceGroupManager
GPUProgram
Mesh
Rendering
ResourceManager
ArchiveFactory
Texture
Plugin
CustomArchiveFactory
HwBufferManager
Renderable
RenderWindow
RenderSystem
Plugin
GLTexture
GLRenderSystem
[285]
C8
Uno de los objetos principales del sistema es el denominado Root. Root proporciona mecanismos para la creacin de los objetos de alto nivel que nos permitirn
gestionar las escenas, ventanas, carga de plugins, etc. Gran parte de la funcionalidad
de Ogre se proporciona a travs del objeto Root. Como se muestra en el diagrama 8.17,
existen otros tres grupos de clases principales en OGRE:
[286]
FontManager
TextureManager
MaterialManager
MeshManager
SkeletonManager
GpuProgramManager
HighLevelGpuProgramManager
RenderSystem
ExternalTextureSourceManager
SceneManager
RenderWindow
ResourceGroupManager
Root
OverlayManager
LogManager
ParticleSystemManager
PlatformManager
DynLibManager
ArchiveManager
Figura 8.18: Diagrama de clases simplicado de los Gestores de alto nivel que dan acceso a los diferentes
subsistemas denidos en OGRE
[287]
Como puede verse en la Figura 8.18, el ResourceManager es una clase abstracta de
la que heredan un gran nmero de Managers, tales como el encargado de las Fuentes,
las Texturas o las Mallas. A continuacin se ofrece una descripcin general de los
Managers (por orden alfabtico) denidos en OGRE. A lo largo del documento se
describirn (y se utilizarn ampliamente) muchos de estos gestores.
Archive Manager. Se encarga de abstraer del uso de cheros con diferentes
extensiones, directorios y archivos empaquetados .zip.
CompositorManager. Esta clase proporciona acceso al framework de composicin y postprocesado de OGRE.
ControllerManager. Es el gestor de los controladores que, como hemos indicado anteriormente, se encargan de producir cambios en el estado de otras
clases dependiendo del valor de ciertas entradas.
DynLibManager. Esta clase es una de las principales en el diseo del sistema
de Plugins de OGRE. Se encarga de gestionar las bibliotecas de enlace dinmico
(DLLs en Windows y objetos compartidos en GNU/Linux).
ExternalTextureSourceManager. Gestiona las fuentes de textura externas (como por ejemplo, en el uso de video streaming).
FontManager. Gestiona las fuentes disponibles para su representacin en superposiciones 2D (ver OverlayManager).
GpuProgramManager. Carga los programas de alto nivel de la GPU, denidos
en ensamblador. Se encarga igualmente de cargar los programas compilados por
el HighLevelGpuProgramManager.
HighLevelGpuProgramManager. Gestiona la carga y compilacin de los programas de alto nivel en la GPU (shaders) utilizados en la aplicacin en HLSL,
GLSL o Cg.
LogManager. Se encarga de enviar mensajes de Log a la salida denida por
OGRE. Puede ser utilizado igualmente por cualquier cdigo de usuario que
quiera enviar eventos de Log.
MaterialManager. Esta clase mantiene las instancias de Material cargadas en
la aplicacin, permitiendo reutilizar los objetos de este tipo en diferentes objetos.
MeshManager. De forma anloga al MaterialManager, esta clase mantiene las
instancias de Mesh permitiendo su reutilizacin entre diferentes objetos.
OverlayManager. Esta clase gestiona la carga y creacin de instancias de superposiciones 2D, que permiten dibujar el interfaz de usuario (botones, iconos,
nmeros, radar...). En general, los elementos denidos como HUDs (Head Up
Display).
ParticleSystemManager. Gestiona los sistemas de partculas, permitiendo aadir gran cantidad de efectos especiales en aplicaciones 3D. Esta clase gestiona
los emisores, los lmites de simulacin, etc.
PlatformManager. Esta clase abstrae de las particularidades del sistema operativo y del hardware subyacente de ejecucin, proporcionando rutinas independientes del sistema de ventanas, temporizadores, etc.
C8
[288]
8.6.2.
Instalacin
El paquete libogre-1.8.0 contiene las bibliotecas necesarias para la ejecucin de las aplicaciones desarrolladas con OGRE. El paquete libogre-dev contiene
los cheros de cabecera instalados en /usr/include/OGRE necesarios para compilar nuestros propios ejemplos. El paquete de documentacin ogre-doc instala en
/usr/share/doc/ogre-doc la documentacin (manual de usuario y API). Finalmente el paquete ogre-tools contiene las herramientas para convertir al formato de
malla binario optimizado de OGRE.
Como se coment en la introduccin, OGRE se centra en proporcionar exclusivamente un motor de despliegue grco 3D interactivo. OGRE no proporciona mecanismos para gestionr la entrada del usuario. En este mdulo utilizaremos OIS (Object
Oriented Input System), una biblioteca desarrollada en C++ multiplataforma que permite trabajar con teclado, ratn, joysticks y otros dispositivos de juego. Instalaremos
igualmente el paquete binario y las cabeceras.
apt-get install libois-1.3.0 libois-dev
SceneManager
Los SceneManagers de OGRE son
implementados como Plugins, de
forma que el usuario puede cargar varios gestores de escena en su
aplicacin. Si el videojuego requiere escenas de interiores con mucha
geometra, as como escenas de exteriores, puede ser interesante cargar dos gestores de escena diferentes optimizados para cada parte del
juego.
[289]
Tanto OGRE como OIS puede ser igualmente instalado en cualquier distribucin
compilando directamente los fuentes. En el caso de OGRE, es necesario CMake para
generar el makele especco para el sistema donde va a ser instalado. En el caso de
OIS, es necesario autotools.
De igual modo, hay que descargar el SDK de DirectX4 . Este paso es opcional
siempre que no queramos ejecutar ningn ejemplo de OGRE que utilice Direct3D.
Sobre Boost...
Boost es un conjunto de bibliotecas
libres que aaden multitud de funcionalidad a la biblioteca de C++
(estn en proceso de aceptacin por
el comit de estandarizacin del
lenguaje).
8.7.
C8
Microsoft Windows
[290]
Para compilar el ejemplo, deniremos un makefile para ambos sistemas operativos. En el siguiente listado se muestra el resultado para GNU/Linux. El formato y uso
de make ser estudiado en detalle a lo largo del primer mdulo del curso.
En la lnea 0 se dene
el nombre del ejecutable que queremos obtener. A continuacin en las lneas 1-3
los directorios para los archivos fuente, objetos
se denen
y cabeceras. Las lneas 8 y 11 denen los ags para la compilacin y enlazado respectivamente.
holaMundo
include *
media
obj *
En las lneas 23-24 se utilizan las funciones de make para generar la lista de
objetos a partir de los fuentes .cpp existentes en el directorio apuntado por DIRSRC.
De este modo, se obtienen los objetos con el mismo nombre que los fuentes, pero
situados en el directorio indicado por DIROBJ.
makefile
makefile.win
[ogre.cfg]
plugins.cfg
resources.cfg
plugins *
src
make mode=release
EXEC := helloWorld
DIRSRC := src/
DIROBJ := obj/
DIRHEA := include/
CXX := g++
# Flags de compilacion ----------------------------CXXFLAGS := -I $(DIRHEA) -Wall pkg-config --cflags OGRE
# Flags del linker -------------------------------LDFLAGS := pkg-config --libs-only-L OGRE
LDLIBS := pkg-config --libs-only-l OGRE -lOIS -lGL -lstdc++
# Modo de compilacion (-mode=release -mode=debug) ----------ifeq ($(mode), release)
CXXFLAGS += -O2 -D_RELEASE
else
CXXFLAGS += -g -D_DEBUG
mode := debug
endif
# Obtencion automatica de la lista de objetos a compilar ------OBJS := $(subst $(DIRSRC), $(DIROBJ), \
$(patsubst %.cpp, %.o, $(wildcard $(DIRSRC)*.cpp)))
.PHONY: all clean
[291]
---------------------------------------------------
>>> Using mode $(mode)
El makele en su versin para plataformas windows se ha nombrado como makele.win. Bsicamente es similar al estudiado para GNU/Linux, pero cambian los
ags de compilacin. Para compilar, ejecutamos el make de MinGW que se denomina
mingw32make:
mingw32-make -f makefile-windows mode=release
Como se muestra en la Figura 8.19, adems de los archivos para make, en el directorio raiz se encuentran tres archivos de conguracin. Estudiaremos a continuacin
su contenido:
ogre.cfg. Este archivo, si existe, intentar ser cargado por el objeto Root. Si el
archivo no existe, o est creado de forma incorrecta, se le mostrar al usuario
una ventana para congurar los parmetros (resolucin, mtodo de anti-aliasing,
profundidad de color, etc...). En el ejemplo que vamos a realizar, se fuerza a que
siempre se abra el dilogo (ver Figura 8.20), recuperando los parmetros que el
usuario especic en la ltima ejecucin.
Plugins.cfg en Windows
En sistemas Windows, como no es
posible crear enlaces simblicos, se
deberan copiar los archivos .dll
al directorio plugins del proyecto e
indicar la ruta relativa a ese directorio en el archivo plugins.cfg.
Esta aproximacin tambin puede
realizarse en GNU/Linux copiando
los archivos .so a dicho directorio.
Nombres en entidades
Si no indicamos ningn nombre,
OGRE elegir automticamente un
nombre para la misma. El nombre
debe ser nico. Si el nombre existe,
OGRE devolver una excepcin.
plugins.cfg. Como hemos comentado anteriormente, un plugin es cualquier mdulo que implementa alguno de los interfaces de plugins de Ogre (como SceneManager o RenderSystem). En este archivo se indican los plugins que debe cargar OGRE, as como su localizacin. En el ejemplo del holaMundo, el
contenido del archivo indica la ruta al lugar donde se encuentran los plugins
(PluginFolder), as como el Plugin que emplearemos (pueden especicarse
varios en una lista) para el rendering con OpenGL.
resources.cfg. En esta primera versin del archivo de recursos, se especica el
directorio general desde donde OGRE deber cargar los recursos asociados a los
nodos de la escena. A lo largo del curso veremos cmo especicar manualmente
ms propiedades a este archivo.
Una vez denida la estructura de directorios y los archivos que forman el ejemplo,
estudiaremos la docena de lneas de cdigo que tiene nuestro Hola Mundo.
Listado 8.2: El main.cpp del Hola Mundo en OGRE
1 #include <ExampleApplication.h>
2
3 class SimpleExample : public ExampleApplication {
C8
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[292]
9
10 int main(void) {
11
SimpleExample example;
12
example.go();
13
14
return 0;
15 }
Como vemos,
en el main se crea una instancia de la clase SimpleExample (denida
en las lneas 2-7 ). Esta clase es derivada de la clase base ExampleApplication, que se
proporciona en el SDK de OGRE para facilitar el aprendizaje de la biblioteca.
En esta clase denimos el mtodo pblico CreateScene. En la lnea 4 llamamos al SceneManager (creado automticamente por la clase base ExampleApplication) a travs de su puntero, solicitando la creacin de una entidad con nombre nico
Sinbad, asociado a Sinbad.mesh (que se encontrar en el directorio especicado en
resources.cfg). Esta llamada nos crear la entidad, y nos devolver un puntero a
un objeto de ese tipo.
A continuacin, adjuntamos la entidad creada a la escena. Para ello, accedemos al
mtodo attachObject() del nodo raz devuelto por getRootSceneNode().
Si compilamos y ejecutamos el holaMundo, primero nos aparecer una ventana
para congurar
las
opciones de rendering (como se muestra en la Figura 8.20). Cuando
pulsemos Accept , se abrir una ventana con el modelo cargado (muy pequeito). Si
pulsamos la echa del cursor , el modelo se acercar. Podemos variar la posicin
de la cmara y la rotacin
del modelo (as
su modo
como
de rendering) con los otros
tres cursores , y las teclas A , S , D , W y R . La salida del modelo se
muestra en la Figura 8.21.
La clase ExampleApplication nos abstrae de la complejidad de crear una aplicacin desde cero con OGRE. Utilizaremos esta clase base en algunos ejemplos ms
del siguiente captulo, pero la abandonaremos rpidamente para controlar todos los
aspectos relativos a la inicializacin (carga de Plugins, denicin del RenderSystem,
gestin de la cmara virtual, control de eventos, etc).
[293]
C8
Figura 8.22: Trabajos nales de los alumnos del Curso de Experto en Desarrollo de Videojuegos en las
Ediciones 2011/2012 y 2012/2013, que utilizaban OGRE como motor de representacin grca. a) Vault
Defense (D. Frutos, J. Lpez, D. Palomares), b) Robocorps (E. Aguilar, J. Alczar, R. Pozuelo, M. Romero),
c) Shadow City (L.M. Garca-Muoz, D. Martn-Lara, E. Monroy), d) Kartoon (R. Garca-Reina, E. Prieto,
J.M. Snchez), e) Ransom (M. Blanco, D. Moreno), f) Crazy Tennis (J. Angulo), g) Camel Race (F.J. Corts),
h) 4 Season Mini Golf (A. Blanco, J. Solana), i) Blood & Metal (J. Garca, F. Gaspar, F. Trivio), j) God
Mode On (J.M. Romero), k) Another Far West (R.C. Daz, J.M. Garca, A. Suarez), l) Impulse (I. Cuesta,
S. Fernndez) y m) Msica Maestro (P. Santos, C. de la Torre).
Captulo
9.1.
Los videojuegos necesitan posicionar objetos en el espacio 3D. El motor grco debe gestionar por tanto la posicin, orientacin y escala de estos objetos (y sus
cambios a lo largo del tiempo). Como vimos en el captulo 8, en grcos interactivos
suelen emplearse representaciones poligionales basadas en tringulos para mejorar
la eciencia. Los vrtices de estos tringulos se representan mediante puntos, y las
normales mediante vectores. Veamos a continuacin algunos conceptos bsicos relacionados con los puntos y los vectores.
Figura 9.1: Aunque en desarrollo
de videojuegos se hace uso de prcticamente todas las reas de las matemticas (desde trigonometra, lgrebra, estadstica o clculo), en este captulo nos centraremos en los
fundamentos ms bsicos del lgebra lineal.
9.1.1.
Sistemas de Referencia
En el inicio del libro Practical Linear Algebra, Farin y Hansford [32] reproducen
un bonito cuento sobre el pueblo de Schilda, perteneciente a Brandeburgo, que ilustra
a la perfeccin la necesidad de distinguir entre Sistemas de Referencia.
295
[296]
9.1.2.
Puntos
Un punto puede denirse como una localizacin en un espacio n-dimensional. Para identicar claramente p como un punto, decimos que p E2 que signica que un
punto 2D vive en el espacio eucldeo E2 . En el caso de los videojuegos, este espacio suele ser bidimensional o tridimensional. El tratamiento discreto de los valores
asociados a la posicin de los puntos en el espacio exige elegir el tamao mnimo y
mximo que tendrn los objetos en nuestro videojuego. Es crtico denir el convenio
empleado relativo al sistema de coordenadas y las unidades entre programadores y
artistas. Existen diferentes sistemas de coordenadas en los que pueden especicarse
estas posiciones, como se puede ver en la Figura 9.4:
Coordenadas Cartesianas. Es sin duda el sistema de coordenadas ms habitual. Este sistema de coordenadas dene ejes perpediculares para especicar la
posicin del punto en el espacio. Como se muestra en la gura 9.3, existen dos
convenios para la denicin de los ejes de coordenadas cartesianas; segn la regla de la mano derecha o de la mano izquierda. Es muy sencillo convertir entre
ambos convenios; basta con invertir el valor del eje que cambia.
SRU
Figura 9.2: Sistemas de referencia
Global (Sistema de Referencia Universal o SRU) y Local. Si describimos la posicin del tesoro con respecto del sistema de coordenadas
local del barco, el tesoro siempre se
mover con el barco y ser imposible volver a localizarlo.
Mano
Izquierda
y
Mano
Derecha
[297]
py
y
ph
p
pz
pr
px
p p
pr
p
p
r
r
Coordenadas
Cilndricas
Coordenadas
Esfricas
9.1.3.
Vectores
Un vector es una tupla n-dimensional que tiene una longitud (denominada mdulo), una direccin y un sentido. Puede ser representado mediante una echa. Dos
vectores son iguales si tienen la misma longitud, direccin y sentido. Los vectores son
entidades libres que no estn ancladas a ninguna posicin en el espacio. Para diferenciar un vector v bidimensional de un punto p, decimos que v R2 ; es decir, que
vive en un espacio lineal R2 .
Convenio en OGRE
En OGRE se utilizan las clases
Vector2 y Vector3 para trabajar indistintamente con puntos y
vectores en 2 y 3 dimensiones.
C9
Coordenadas
Cartesianas
[298]
b
a+
Mdulo y normalizacin
c-d
Si dividimos un vector por su longitud (es decir, escalamos el vector a por el factor
1/|a|), obtenemos como resultado un vector con la misma direccin y sentido, pero de
mdulo la unidad (vector unitario). Esta operacin se denomina normalizacin, y da
como resultado un vector normalizado que se emplea en gran cantidad de algoritmos
en programacin de videojuegos.
aN
a
=
|a|
Producto Escalar
El producto escalar (dot product) es conmutativo (a b = b a), y se calcula como:
(9.1)
Tambin puede ser calculado mediante la siguiente expresin que relaciona el ngulo que forman ambos vectores.
a b = |a| |b| cos()
a
-a
e
e
2e
Los vectores pueden multiplicarse entre s, pero a diferencia de los escalares, admiten dos operaciones de multiplicacin; el producto escalar (que da como resultado
un escalar), y el producto vectorial (que lgicamente da como resultado otro vector).
Veamos a continuacin cmo se denen estas dos operaciones y algunas de sus aplicaciones prcticas.
a b = ax bx + ay by + az bz
(9.2)
Combinando las expresiones 9.1 y 9.2, puede calcularse el ngulo que forman dos
vectores (operacin muy utilizada en informtica grca).
a b
[299]
Otra operacin muy til es el clculo de la proyeccin de un vector sobre otro, que
se dene a b como la longitud del vector a que se proyecta mediante un ngulo
recto sobre el vector b (ver Figura 9.6).
a b = |a| cos() =
ab
|b|
a
ab>0
b
ab=0
b
Misma direccin. Si el ngulo que forman entre ambos vectores est entre 270
y 90 grados, por lo que a b > 0 (ver Figura 9.7).
Direccin Opuesta. Si el ngulo que forman entre ambos vectores es mayor que
90 grados y menor que 270, por lo que a b < 0.
a
b
ab<0
Esta interpretacin de misma direccin y direccin opuesta no es literal. Nos servir para comprobar si el vector normal de una cara poligonal est de frente a la cmara
virtual (si tiene direccin opuesta al vector look) de la cmara, o por el contrario est
siendo visto de espaldas.
Producto Vectorial
Mediante el producto vectorial (cross product) de dos vectores se obtiene otro
vector que es perpendicular a los dos vectores originales.
a b = [(ay bz az by ), (az bx ax bz ), (ax by ay bx )]
El sentido del vector resultado del producto escalar sigue la regla de la mano derecha (ver Figura 9.8): si agarramos a b con la palma de la mano, el pulgar apunta
en el sentido positivo del vector producto si el giro del resto de los dedos va de a a b.
En caso contrario, el sentido del vector es el inverso. Por tanto, el producto vectorial
no es conmutativo.
axb
a
El mdulo del producto vectorial est directamente relacionado con el ngulo que
forman ambos vectores . Adems, el mdulo del producto vectorial de dos vectores
es igual al rea del paralelogramo formado por ambos vectores (ver Figura 9.8).
|a b| = |a| |b| sin
C9
El producto escalar se emplea adems para detectar si dos vectores tienen la misma direccin, o si son perpendiculares, o si tienen la misma direccin o direcciones
opuestas (para, por ejemplo, eliminar geometra que no debe ser representada).
[300]
A continuacin describiremos las transformaciones geomtricas ms comunes empleadas en grcos por computador. Para introducir los conceptos generales asociados
a las transformaciones, comenzaremos con una discusin sobre las operaciones en 2D
para pasar a la notacin matricial 2D y, posteriormente, a la generalizacin tridimensional empleando coordenadas homogeneas.
9.2.
Transformaciones Geomtricas
py = py + ty
py = d sen
(9.4)
x
Figura 9.9: Arriba. Traslacin de
un punto p a p empleando el vector t. Abajo. Es posible trasladar un
objeto poligonal completo aplicando la traslacin a todos sus vrtices.
p'
p'
px = px cos py sen
(9.3)
De igual modo podemos expresar una rotacin de un punto p = (x, y) a una nueva
posicin rotando un ngulo respecto del origen de coordenadas, especicando el eje
de rotacin y un ngulo . Las coordenadas iniciales del punto se pueden expresar
como (ver Figura 9.10):
px = d cos
(9.5)
p
x
[301]
De forma similar, un cambio de escala de un objeto bidimensional puede llevarse
a cabo multiplicando las componentes x, y del objeto por el factor de escala Sx , Sy
en cada eje. As, como se muestra en la Figura 9.11 un cambio de escala se puede
expresar como:
py = py Sy
(9.6)
px = px Sx
p'
x
Cuando queremos cambiar la localizacin de un objeto, habitualmente necesitamos especicar una combinacin de traslaciones y rotaciones en el mismo (por
ejemplo, cuando cogemos el telfono mvil de encima de la mesa y nos lo guardamos
en el bolsillo, sobre el objeto se aplican varias traslaciones y rotaciones). Es interesante
por tanto disponer de alguna representacin que nos permita combinar transformaciones de una forma eciente.
9.2.1.
Figura 9.11: Conversin de un cuadrado a un rectngulo empleando los factores de escala Sx =
2, Sy = 1,5.
Homogenezate!!
Gracias al uso de coordenadas homogneas es posible representar las
ecuaciones de transformacin geomtricas como multiplicacin de
matrices, que es el mtodo estndar en grcos por computador (soportado en hardware por las tarjetas
aceleradoras grcas).
C9
Representacin Matricial
Puntos y Vectores
En el caso de puntos, la componente homognea h = 1. Los vectores
emplean como parmetro h = 0.
x
y
1
(9.7)
Al resolver la multiplicacin matricial se obtienen un conjunto de ecuaciones equivalentes a las enunciadas en 9.3. De forma anloga, las operaciones de rotacin Tr y
escalado Ts tienen su equivalente matricial homogneo.
cos
Tr = sen
0
sen
cos
0
0
0
1
Ts =
Sx
0
0
0
Sy
0
0
0
1
(9.8)
[302]
Las transformaciones en el espacio 3D requieren simplemente aadir el parmetro homogneo y describir las matrices (en este caso 4x4). As, las traslaciones Tt y
escalados Ts en 3D pueden representarse de forma homognea mediante las siguientes
matrices:
Tx
Ty
Tz
1
1 0 0
0 1 0
Tt = 0 0 1
0 0 0
Sx
0
Ts = 0
0
0
Sy
0
0
0
0
Sz
0
0
0
0
1
(9.9)
Las rotaciones requieren distinguir el eje sobre el que se realizar la rotacin. Las
rotaciones positivas alrededor de un eje se realizan en sentido opuesto a las agujas
del reloj, cuando se est mirando a lo largo de la mitad positiva del eje hacia el origen
del sistema de coordenadas (ver Figura 9.12).
Las expresiones matriciales de las rotaciones son las siguientes:
1
0
Rx = 0
0
0
cos
sen
0
0
sen
cos
0
0
0
0
1
cos
0
Ry = sen
0
0
1
0
0
sen
0
cos
0
0
0
0
1
cos
sen
Rz = 0
0
sen
cos
0
0
Las tres transformaciones estudiadas (traslacin, rotacin y escalado) son ejemplos de transformaciones anes, en las que cada una de las coordenadas transformadas se pueden expresar como una funcin lineal de la posicin origen, y una serie de
constantes determinadas por el tipo de transformacin.
Una subclase de las transformaciones anes, es la formada por las transformaciones lineales, que permiten aplicar todas las transformaciones mediante multiplicaciones. Como hemos visto anteriormente, las traslacin anes no homognea no es
una operacin lineal (porque no puede realizarse mediante una multiplicacin). Las
transformaciones anes mantienen la mayora de las propiedades de los objetos, excepto los ngulos y las distancias. Aplicando transformaciones anes se mantiene la
colineridad (ver Tabla 9.1), por lo que las lneas paralelas seguirn siendo paralelas.
Como caso particular de estudio, las transformaciones de cuerpo rgido preservan
todas las propiedades geomtricas de los objetos. Cualquier combinacin de rotaciones y traslaciones homogneas son transformaciones de cuerpo rgido.
Cuadro 9.1: Propiedades geomtricas preservadas segn la clase de transformacin: Transformaciones
anes, Lineales y de Cuerpo Rgido.
Propiedad
ngulos
Distancias
Ratios de distancias
Lneas paralelas
Lneas rectas
T. Anes
T. Lineales
T. Cuerpo Rgido
No
No
S
S
S
No
No
S
S
S
S
S
S
S
S
0 0
0 0
1 0
0 1
(9.10)
Afines
Lineales
Escalado
C. Rg.
Traslacin
Rotacin
[303]
T(-p)
Rz()
T(p)
Figura 9.14: Secuencia de operaciones necesarias para rotar una gura respecto de un origen arbitrario.
9.2.2.
Transformaciones Inversas
(9.11)
Tanto la matriz A como su inversa deben ser cuadradas y del mismo tamao. Otra
propiedad interesante de la inversa es que la inversa de la inversa de una matriz es
igual a la matriz original (A1 )1 = A.
En la ecuacin inicial, podemos resolver el sistema utilizando la matriz inversa.
Si partimos de A = B x, podemos multiplicar ambos trminos a la izquierda por la
inversa de B, teniendo B 1 A = B 1 B x, de forma que obtenemos la matriz
identidad B 1 A = I x, con el resultado nal de B 1 A = x.
9.2.3.
Composicin
Como hemos visto en la seccin anterior, una de las principales ventajas derivadas
del trabajo con sistemas homogneos es la composicin de matrices. Matemticamente esta composicin se realiza multiplicando las matrices en un orden determinado, de forma que es posible obtener la denominada matriz de transformacin neta
MN resultante de realizar sucesivas transformaciones a los puntos. De este modo,
bastar con multiplicar la MN a cada punto del modelo para obtener directamente su
posicin nal. Por ejemplo, si P es el punto original y P es el punto transformado, y
T1 Tn son transformaciones (rotaciones, escalados, traslaciones) que se aplican al
C9
[304]
De este modo, si se quiere realizar una transformacin respecto de un punto distinto a ese origen del SRU, habr que hacer coincidir primero el punto con el origen
del sistema de referencia, aplicar la transformacin y devolver el objeto a su posicin
original. As, en el ejemplo de la Figura 9.14 si queremos rotar el objeto respecto del
punto p es necesario, primero trasladar el objeto para que su origen quede alineado con
el origen del SRU, luego aplicar la rotacin, y nalmente aplicar la traslacin inversa.
As, tenemos MN = Tp Rz Tp .
9.3.
px =
d px
pz
(9.12)
x
Figura 9.16: Resultado de aplicar directamente la rotacin Rz ()
respecto del SRU. Puede comprobarse el diferente resultado obtenido en comparacin con el de la gura 9.14.
Rz(/4)
Sx(2)
C9
[305]
Sx(2)
Rz(/4)
Figura 9.17: La multiplicacin de matrices no es conmutativa, por lo que el orden de aplicacin de las
transformaciones es relevante para el resultado nal. Por ejemplo, la gura de arriba primero aplica una
rotacin y luego el escalado, mientras que la secuencia inferior aplica las transformaciones en orden inverso.
1
0
p = Mp p = 0
0
Denicin near y far
Un error comn suele ser que la denicin correcta de los planos near
y far nicamente sirve para limitar
la geometra que ser representada
en el Frustum. La denicin correcta de estos parmetros es imprescindible para evitar errores de precisin en el Z-Buffer.
0
1
0
0
0
0
1
1/d
0
px
d px /pz
px
0 py py d py /pz
0 pz = pz = d
1
0
pz /d
1
(9.13)
El ltimo paso de la ecuacin 9.13 corresponde con la normalizacin de los componentes dividiendo por el parmetro homogneo h = pz /d. De esta forma tenemos
la matriz de proyeccin que nos aplasta los vrtices de la geometra sobre el plano de
proyeccin. Desafortunadamente esta operacin no puede deshacerse (no tiene inversa). La geometra una vez aplastada ha perdido la informacin sobre su componente
de profundidad. Es interesante obtener una trasnformacin en perspectiva que proyecte los vrtices sobre el cubo unitario descrito previamente (y que s puede deshacerse).
De esta forma, denimos la pirmide de visualizacin o frustum, como la pirmide
truncada por un plano paralelo a la base que dene los objetos de la escena que sern
representados. Esta pirmide de visualizacin queda denida por cuatro vrtices que
denen el plano de proyeccin (left l, right r, top t y bottom b), y dos distancias a los
planos de recorte (near n y far f ), como se representa en la Figura 9.18. El ngulo de
visin de la cmara viene determinado por el ngulo que forman l y r (en horizontal)
y entre t y b (en vertical).
[306]
botto
m
right
left
top
Mp
near
far
La matriz que transforma el frustum en el cubo unitario viene dada por la expresin
de la ecuacin 9.14.
2n
rl
0
Mp =
0
0
0
2n
tb
0
0
r+l
rl
t+b
tb
f +n
f n
0
0
n
f2fn
(9.14)
9.4.
Cuaternios
Teorema de Euler
Segn el Teorema de Rotacin de
Leonhard Euler (1776), cualquier
composicin de rotaciones sobre un
slido rgido es equivalente a una
sola rotacin sobre un eje, llamado
Polo de Euler.
9.4. Cuaternios
b)
c)
d)
Figura 9.20: El problema del bloqueo de ejes (Gimbal Lock). En a) se muestra el convenio de sistema de
coordenadas que utilizaremos, junto con las elipses de rotacin asociadas a cada eje. En b) aplicamos una
rotacin respecto de x, y se muestra el resultado en el objeto. El resultado de aplicar una rotacin de 90o
en y se muestra en c). En esta nueva posicin cabe destacar que, por el orden elegido de aplicacin de las
rotaciones, ahora el eje x y el eje z estn alineados, perdiendo un grado de libertad en la rotacin. En d) se
muestra cmo una rotacin en z tiene el mismo efecto que una rotacin en x, debido al efecto de Gimbal
Lock.
Como vimos en la seccin 9.2.1, se puede usar una matriz para resumir cualquier
conjunto de rotaciones en 3D. Sin embargo, las matrices no son la mejor forma de
representar rotaciones, debido a:
1. Almacenamiento. La representacin mediante matrices require nueve valores
de punto otante para almacenar una rotacin. Segn el Teorema de Rotacin
de Euler es suciente con establecer la rotacin frente a un nico eje (el Polo
de Euler).
2. Tiempo de Cmputo. En la composicin de la matriz con un vector, se necesitan calcular nueve multiplicaciones y seis sumas en punto otante.
3. Interpolacin. En muchas ocasiones es necesario calcular valores intermedios
entre dos puntos conocidos. En el caso de rotaciones, es necesario calcular los
valores de rotacin intermedios entre dos rotaciones clave (en la animacin de la
rotacin de una cmara virtual, o en las articulaciones de un personaje animado).
La interpolacin empleando matrices es muy complicada.
Figura 9.21: Los cuaternios unitarios pueden ser representados como puntos sobre la esfera unitaria.
La interpolacin esfrica lineal entre dos cuaternios p y q se puede
igualmente representar como un arco sobre la esfera..
C9
a)
[307]
[308]
9.4.1.
Suma y Multiplicacin
9.4.2.
Inversa
En el caso de cuaternios unitarios, la inversa del cuaternio q 1 es igual al conjugado q . El conjugado se calcula simplemente negando la parte vectorial, por lo que,
trabajando con cuaternios unitarios, podemos calcularlos simplemente como:
q 1 = q = [qv , qs ]
La multiplicacin de un cuaternio por su inversa obtiene como resultado el escalar
1 (es decir, rotacin 0). De este modo, q q 1 = [0 0 0 1].
9.4.3.
Asociatividad
La concatenacin de cuaternios
cumple la propiedad asociativa, por
lo que es posible obtener la expresin del cuaternio neto y aplicarlo a
varios objetos de la escena.
[309]
Es habitual realizar conversiones entre matrices y cuaternios. OGRE incorpora llamadas a su biblioteca (ver seccin 9.6) para realizar esta operacin. Se
recomienda el artculo de Gamasutra de Nick Bobic Rotating Objects Using
Quaternions para profundizar en su uso.
Una de las operaciones ms empleadas en grcos por computador es la interpolacin. Un claro ejemplo de su uso es para calcular la posicin intermedia en una
animacin de un objeto que se traslada desde un punto A, hasta un punto B.
6
5
4
3
2
1
B
v
A
1
2 3
4 5
6 7
cuaternio q
(0.707, 0, 0.707, 0)
y
y
x
cuaternio p
(0.707, 0, 0, 0.707)
Figura 9.23: Ejemplo de aplicacin de interpolacin esfrica SLERP en la rotacin de dos cuaternios p y q
sobre un objeto.
sin((1 t))
sin(t)
p+
q
sin()
sin()
C9
9.5.
[310]
9.6.
La biblioteca de clases de OGRE proporciona una amplia gama de funciones matemticas para trabajar con matrices, vectores, cuaternios y otras entidades matemticas
como planos, rayos, esferas, etc...
La documentacin relativa a todas estas clases se encuentra en el Mdulo Math,
que forma parte del ncleo (Core) de OGRE. A continuacin se muestra un sencillo
ejemplo que utiliza algunos operadores de la clase Vector3 y Quaternion.
Documentacin...
Recordemos que en distribuciones
de GNU/Linux basadas en Debian,
puedes encontrar la documentacin de OGRE en local en
/usr/share/doc/ogre-doc.
cout << " Ejemplo de algunas clases de Math en OGRE " << endl;
cout << "-------------------------------------------" << endl;
Vector3 v1(1.0, 0.0, 0.0);
Vector3 v2(0.0, 2.0, 0.0);
Quaternion p(0.707107, 0.0, 0.0, 0.707107);
Quaternion q(Degree(90), Vector3(0.0, 1.0, 0.0));
cout
cout
cout
cout
<<
<<
<<
<<
"
"
"
"
Vector V1
Vector V2
Cuaternio
Cuaternio
=
=
P
Q
"
"
=
=
cout
cout
cout
cout
cout
cout
cout
cout
[311]
Modulo: |V1| = 1
Normalizar: V2n = Vector3(0, 1, 0)
Angulo entre V1 y V2 = 90
--- Algunos operadores de Cuaternios ---Suma: P + Q = Quaternion(1.41421, 0, 0.707107, 0.707107)
Producto: P * Q = Quaternion(0.5, -0.5, 0.5, 0.5)
Producto escalar: P * Q = 0.5
SLERP(p,q,0.5) = Quaternion(0.816497, 0, 0.408248, 0.408248)
B'
A'
C'
B
A
C
Ejercicios Propuestos
1. Dado el polgono ABC, con A=(1,1,3), B=(3,1,3) y C=(1,1,1), calcular el restultado de aplicar una traslacin de -2 unidades en el eje X, y 1 unidad en el eje Z,
y despus una rotacin de /2 radianes respecto del eje Z. Las transformaciones
se realizan en ese orden y sobre el SRU.
2. Dado el cuadrado denido por los puntos A=(1,0,3), B=(3,0,3), C=(3,0,1) y
D=(1,0,1), realizar un escalado del doble en el eje X respecto a su centro geomtrico. Compruebe que el resultado no es el mismo que si realiza el escalado
directamente con respecto del eje X del SRU.
3. Cules seran las expresiones matriciales correspondientes a la operacin de
reexin (espejo) sobre los planos ortogonales x=0, y=0 y z=0? Calcule una
matriz por cada plano.
4. Dado el tringulo ABC, siendo A=(1, -1, 1), B=(-1, -1, -1) y C=(1, 1, -1), desplace la cara poligonal 2 unidades en la direccin de su vector normal (ver Figura
9.24). Suponga que el vector normal sale segn la regla de la mano derecha.
C9
9.7.
Captulo
10
Grafos de Escena
Carlos Gonzlez Morcillo
omo vimos en el captulo 8, uno de los pilares clave de cualquier motor grco
es la organizacin de los elementos que se representarn en la escena. Esta
gestin se realiza mediante el denominado Grafo de Escena que debe permitir
inserciones, bsquedas y mtodos de ordenacin ecientes. OGRE proporciona una
potente aproximacin a la gestin de los Grafos de Escena. En este captulo trabajaremos con los aspectos fundamentales de esta estructura de datos jerrquica.
10.1.
Justicacin
Como seala D. Eberly [30], el motor de despliegue grco debe dar soporte a
cuatro caractersticas fundamentales, que se apoyan en el Grafo de Escena:
Test de Visibilidad
La gestin de la visibilidad de los
elementos de la escena es una de las
tareas tpicas que se encarga de realizar el motor grco empleando el
Grafo de Escena.
313
[314]
3. Facilidad de Uso. Esta caracterstica est directamente relacionada con la anterior. El Grafo de Escena forma parte de la especicacin del middleware de
despliegue grco que facilita el uso al programador nal del videojuego. De
este modo, el programador se centra en la semntica asociada a la utilizacin
del Grafo, dejando de lado los detalles de representacin de bajo nivel.
4. Extensibilidad. En gran cantidad de videojuegos comerciales, los equipos de
trabajo de programacin frecuentemente se quejan del cambio en la especicacin de requisitos iniciales por parte del equipo artstico. Esto implica aadir
soporte a nuevos tipos de geometra, efectos visuales, etc. El motor debe estar preparado para aadir los nuevos tipos sin necesidad de cambios profundos
en la arquitectura. El diseo correcto del Grafo de Escena mitiga los problemas asociados con la extensibilidad y la adaptacin al cambio en los requisitos
iniciales.
Las estructuras de datos de Grafos de Escena suelen ser grafos que representen
las relaciones jerrquicas en el despliegue de objetos compuestos. Esta dependencia
espacial se modela empleando nodos que representan agregaciones de elementos (2D
o 3D), as como transformaciones, procesos y otras entidades representables (sonidos,
vdeos, etc...).
El Grafo de Escena puede denirse como un grafo dirigido sin ciclos. Los arcos
del grafo denen dependencias del nodo hijo respecto del padre, de modo que la aplicacin de una transformacin en un nodo padre hace que se aplique a todos los nodos
hijo del grafo.
El nodo raz habitualmente es un nodo abstracto que proporciona un punto de
inicio conocido para acceder a la escena. Gracias al grafo, es posible especicar fcilmente el movimiento de escenas complejas de forma relativa a los elementos padre
de la jerarqua. En la Figura 10.1, el movimento que se aplique a la Tierra (rotacin,
traslacin, escalado) se aplicar a todos los objetos que dependan de ella (como por
ejemplo a las Tiritas, o las Llantas de la exacavadora). A su vez, las modicaciones
aplicadas sobre la Cabina se aplicarn a todos los nodos que herenden de la jerarqua,
pero no al nodo Llantas o al nodo Tierra.
10.1.1.
Cabina
Llantas
Base
Codo
Pala
Tierra
Escena
Tierra
Tiritas
Cielo
Llantas
Cabina
Base
Figura 10.1: Ejemplo de grafo asociado a la escena de la excavadora. Escena diseada por Manu Jrvinen.
[315]
Figura 10.2: Ejemplo de estructura de datos para la gestin del culling jerrquico. En el ejemplo, el nodo
referente a las Llantas (ver Figura 10.1) debe mantener la informacin de la caja lmite (Bounding Box) de
todos los hijos de la jerarqua, remarcada en la imagen de la derecha.
Culling. En esta operacin se estudia la visibilidad de los elementos contenidos en los nodos. Gracias a la especicacin jerrquica del grafo, es posible
podar ramas completas de la jerarqua. Para realizar esta operacin, se utilizan
estructuras de datos adicionales que mantienen informacin sobre las coordenadas lmite (mximo y mnimo) asociadas a cada objeto. As, consultando estos lmites, si un nodo queda totalmente fuera de la pirmide de visualizacin
(Frustum), implicar que todos sus nodos hijo estn igualmente fuera y no ser
necesario su posterior dibujado (ver Figura 10.2).
Dibujado. En esta operacin se aplican los algoritmos de rendering a cada nodo
de la jerarqua (comenzando por la raz, bajando hasta las hojas). Si un nodo
contiene rdenes que cambien el modo de dibujado, se aplicarn igualmente a
todos los nodos hijo de la jerarqua.
A continuacin estudiaremos el interfaz de alto nivel que proporciona OGRE para
la gestin de Grafos de Escena. Veremos las facilidades de gestin orientada a objetos, as como la abstraccin relativa a los tipos de datos encapsulados en cada nodo.
Tareas del Gestor
En este captulo nos centraremos en
la parte especca de aplicacin de
transformaciones a los objetos. A
lo largo del mdulo estudiaremos
otros aspectos relacionados con el
gestor, como el tratamiento del culling o la gestin de sombras dinmicas.
10.2.
C10
[316]
10.2.1.
Creacin de Objetos
Destruccin de nodos
La destruccin de un nodo de la escena no implica la liberacin de la
memoria asignada a los objetos adjuntos al nodo. Es responsabilidad
del programador liberar la memoria
de esos objetos.
Root
MovableObject
Hijo1
SimpleRenderable
Entity
Light
AnimableObject
ParticleSystem
Hijo11
Rectangle2D
WireBoundingBox
Frustum
Hijo2
Hijo12
ManualObject
Hijo121
Camera
Figura 10.4: Algunos de los tipos de objetos que pueden ser aadidos a un nodo en OGRE. La clase
abstracta MovableObject dene pequeos objetos que sern aadidos (attach) al nodo de la escena.
[317]
La operacin de aadir un nodo hijo se realiza mediante la llamada al mtodo
addChild(Node *child) que aade el nodo hijo previamente creado al nodo existente. La clase SceneNode es una subclase de la clase abstracta Node, denida para
contener informacin sobre las transformaciones que se aplicarn a los nodos hijo. De
este modo, la transformacin neta que se aplica a cada hijo es el resultado de componer
las de sus padres con las suyas propias.
La clase Entity se emplea para la gestin de pequeos objetos mviles basados en mallas poligonales. Para la denicin del escenario (habitualmente con gran
complejidad poligonal e inmvil) se emplea la clase StaticGeometry. La creacin
de entidades se realiza empleando el mtodo createEntity del Gestor de Escena,
indicando el nombre del modelo y el nombre que quiere asignarse a la entidad.
Root
myNode
myEnt
Figura 10.5: Jerarqua obtenida en
el grafo de escena asociado al listado de ejemplo.
3-4
,
y
aade
la entidad myEnt
en
las
lneas
un nodo hijo myNode de RootSceneNode
al nodo myChild en la lnea 6 (ver Figura 10.5). A partir de ese punto del cdigo, cualquier transformacin que se aplique sobre el nodo myNode se aplicarn a la
entidad myEnt.
Listado 10.1: Creacin de nodos
1 class SimpleExample : public ExampleApplication {
2
public : void createScene() {
3
SceneNode* node = mSceneMgr->createSceneNode("myNode");
4
mSceneMgr->getRootSceneNode()->addChild(node);
5
Entity *myEnt = mSceneMgr->createEntity("cuboejes", "cuboejes.
mesh");
6
node->attachObject(myEnt);
7
}
8 };
10.2.2.
Transformaciones 3D
C10
Como hemos visto antes, los nodos de la escena contienen objetos. OGRE dene
una clase abstracta llamada MovableObject para denir todos los tipos de objetos
que pueden ser aadidos a los nodos de la escena. En la Figura 10.4 se denen algunos
de las principales subclases de esta clase abstracta. Para aadir un objeto a un nodo de
la escena, se emplea la llamada al mtodo attachObject(MovableObject *obj).
[318]
Root
node2
z node1
z node3
node1
ent1
node3
a)
b)
nod
e4
Figura 10.6: Ejemplo de transformaciones empleando el Grafo de Escena de OGRE. a) Resultado de ejecucin del ejemplo. b) Representacin de los nodos asociados a la escena. c) Jerarqua del grafo de escena.
ent3
c)
node2
ent2
node4
ent4
Aunque es posible aplicar transformaciones al nodo Root, se desaconseja su uso. El nodo Root debe mantenerse esttico, como punto de referencia del SRU.
10.2.3.
Espacios de transformacin
Las transformaciones estudiadas anteriormente pueden denirse relativas a diferentes espacios de transformacin. Muchos de los mtodos explicados en la seccin
anterior admiten un parmetro opcional de tipo TransformSpace que indica el espacio de transformacin relativo de la operacin. OGRE dene tres espacios de trasnformacin como un tipo enumerado Node::TransformSpace:
TS_LOCAL. Sistema de coordenadas local del nodo.
TS_PARENT. Sistema de coordenadas del nodo padre.
TS_WORLD. Sistema de coordenadas universal del mundo.
El valor por defecto de este espacio de transformacin depende de la operacin a
realizar. Por ejemplo, la rotacin (ya sea mediante la llamada a rotate o a pitch, yaw o
roll) se realiza por defecto con respecto al sistema de local, mientras que la traslacin
se realiza por defecto relativa al padre.
Veamos en el siguiente listado un ejemplo que ilustre estos conceptos. En el listado
se denen tres nodos, en el que node1 es el padre de node2
y node3. A node 2 se
le aplica una rotacin respecto del eje y en la lnea 15 . Como hemos comentado
anteriormente, por defecto (si no se le indica ningn parmetro adicional)
se realiza
sobre TS_LOCAL1 Posteriormente se aplica una traslacin en la lnea 16 , que por
defecto se realiza relativa al sistema de coordenadas del nodo padre. Al node3 se el
aplican exactamente la misma rotacin y traslacin,
pero sta ltima se aplica respecto
del sistema de referencia local (lnea 23 ). Como puede verse en la Figura 10.7, la
entidad adjunta al node3 se representa trasladada 5 unidades respecto de su sistema de
coordenadas local.
Listado 10.3: Uso de diferentes espacios de transformacin
1 class SimpleExample : public ExampleApplication {
2
public : void createScene() {
1 Especicar el valor por defecto del espacio de transformacin tiene el mismo efecto que no
especicarlo. Por ejemplo, cambiar la lnea 15 del ejemplo por node2->yaw(Degree(-90),
Node::TS_LOCAL); no tendra ningn efecto diferente sobre el resultado nal.
C10
Posicin de Root
[319]
[320]
a)
y
x
z
no d
node1
x node2
e3
Figura 10.7: Ejemplo de trabajo con diferentes espacios de transformacin. a) Resultado de ejecucin del
ejemplo. b) Distribucin de los nodos del Grafo de Escena.
3
SceneNode* node1 = mSceneMgr->createSceneNode("Node1");
4
Entity *ent1 = mSceneMgr->createEntity("ent1", "cuboejes.mesh");
5
node1->attachObject(ent1);
6
mSceneMgr->getRootSceneNode()->addChild(node1);
7
node1->setPosition(0,0,480);
8
node1->yaw(Degree(-45));
// Por defecto es Node::TS_LOCAL
9
node1->pitch(Degree(45)); // Por defecto es Node::TS_LOCAL
10
11
SceneNode* node2 = mSceneMgr->createSceneNode("Node2");
12
Entity *ent2 = mSceneMgr->createEntity("ent2", "cuboejes.mesh");
13
node2->attachObject(ent2);
14
node1->addChild(node2);
15
node2->yaw(Degree(-90)); // Por defecto es Node::TS_LOCAL
16
node2->translate(5,0,0); // Por defecto es Node::TS_PARENT
17
18
SceneNode* node3 = mSceneMgr->createSceneNode("Node3");
19
Entity *ent3 = mSceneMgr->createEntity("ent3", "cuboejes.mesh");
20
node3->attachObject(ent3);
21
node1->addChild(node3);
22
node3->yaw(Degree(-90)); // Por defecto es Node::TS_LOCAL
23
node3->translate(5,0,0, Node::TS_LOCAL); // Cambiamos a LOCAL!
24
}
25 };
y
z
x
no de 1
x node2
node3
Figura 10.8: Tras aplicar la traslacin de la lnea 23, se aplica la rotacin local al node3. Ahora el objeto
del node2 y node3 aparecen alineados exactamente en la misma posicin del espacio.
Captulo
11
11.1.
Formatos de Especicacin
11.1.1.
Introduccin
En la actualidad, el desarrollo de videojuegos de ltima generacin implica la manipulacin simultnea de mltiples cheros de diversa naturaleza, como son imgenes
estticas (BMP (BitMaP), JPG, TGA (Truevision Graphics Adapter), PNG, ...), cheros de sonido (WAV (WAVeform), OGG, MP3 (MPEG-2 Audio Layer III)), mallas que
representan la geometra de los objetos virtuales, o secuencias de vdeo (AVI (Audio
Video Interleave), BIK (BINK Video), etc). Una cuestin relevante es cmo se cargan
estos cheros y qu recursos son necesarios para su reproduccin, sobre todo teniendo
en cuenta que la reproduccin debe realizarse en tiempo real, sin producir ningn tipo
de demora que acabara con la paciencia del usuario.
321
[322]
Figura 11.1: Flujo de datos desde los archivos de recursos hasta los subsistemas que se encargan de la
reproduccin de los contenidos [62].
[323]
Normalmente estos archivos estn asociados a un nivel del juego. En cada uno de
estos niveles se denen una serie de entornos, objetos, personajes, objetivos, eventos,
etc. Al cambiar de nivel, suele aparecer en la pantalla un mensaje de carga y el usuario
debe esperar; el sistema lo que est haciendo en realidad es cargar el contenido de
estos cheros que empaquetan diversos recursos.
Cada uno de los archivos empaquetados se debe convertir en un formato adecuado
que ocupe el menor nmero de recursos posible. Estas conversiones dependen en la
mayora de los casos de la plataforma hardware en la que se ejecuta el juego. Por
ejemplo, las plataformas PS3 y Xbox360 presentan formatos distintos para el sonido
y las texturas de los objetos 3D.
11.1.2.
Los juegos actuales con al menos una complejidad media-alta suelen ocupar varios
GigaBytes de memoria. La mayora de estos juegos constan de un conjunto de cheros
cerrados con formato privado que encapsulan mltiples contenidos, los cuales estn
distribuidos en varios DVDs (4.7 GB por DVD) o en un simple Blue-Ray ( 25GB)
como es en el caso de los juegos vendidos para la plataforma de Sony - PS3. Si tenemos algn juego instalado en un PC, tenemos acceso a los directorios de instalacin y
podemos hacernos una idea del gran tamao que ocupan una vez que los juegos han
sido instalados.
Lo que vamos a intentar en las siguientes secciones es hacernos una idea de cmo
estos datos se almacenan, qu formatos utilizan y cmo se pueden comprimir los datos
para obtener el producto nal. Por tanto, lo primero que debemos distinguir son los
tipos de cheros de datos que normalmente se emplean. La siguiente clasicacin,
ofrece una visin general [62]:
Objetos y mallas 3D para el modelado de entornos virtuales: para el almacenamiento de este tipo de datos, normalmente son necesarias unas pocas decenas
de megabytes para llevar a cabo dicha tarea. En este tipo de cheros se almacena
toda la geometra asociada al videojuego.
Mallas 3D y datos de animacin: estos datos en realidad no ocupan demasiado
espacio pero suele suponer un nmero elevado de cheros, la suma de todos
ellos puede ocupar varias decenas de MB tambin.
Mapa/ Datos de nivel: en este tipo de archivos se almacenan disparadores de
eventos (eventos que se pueden producir en un determinado escenario y asociacin de acciones a realizar una vez que se producen), Tipos de objetos del
entorno, scripts, etc. No ocupan demasiado espacio al igual que el caso anterior, y suele ser bastante sencillo compactarlos o comprimirlos.
Sprites (personajes) y texturas asociadas a los materiales: suele haber bastante informacin asociada a estos tipos de datos. En un juego medianamente
complejo, los cheros de este tipo comienzan enseguida a ocupar bastante espacio, hablamos de cientos de megas.
Sonido, msica y dilogos: suelen ser los datos que ocupan mas espacio de
todo el juego, sobre todo cuando los juegos relatan una profunda y larga historia.
Vdeo y escenas pre-grabadas: Cuando se usa este tipo de recursos suele ocupar la mayora de espacio, por eso se usan con moderacin. Suelen ser la combinacin de personajes animados con archivos de sonido.
C11
En la siguiente seccin se ver con mayor grado de detalle los recursos grcos 3D
que son necesarios, centrndonos en los formatos y los requisitos de almacenamiento.
[324]
En las siguientes secciones se describir con mayor detalle cada uno de los puntos
anteriores.
Objetos y Mallas 3D
Al contrario de lo que normalmente se puede pensar, la geometra de los elementos
tridimensionales que se emplean en un videojuego no ocupan demasiado espacio en
comparacin con otro tipo de contenidos [62]. Como se coment en las secciones
anteriores, los principales consumidores de espacio son los cheros de audio y vdeo.
Una malla 3D, independientemente de que corresponda con un personaje, objeto
o entorno, es una coleccin de puntos situados en el espacio, con una serie de datos
asociados que describen cmo estos puntos estn organizados y forman un conjunto
de polgonos y cmo stos deben ser renderizados.
Por otro lado, a los puntos situados en el espacio se les llama vrtices y se representan mediante tres puntos pertenecientes a las tres coordenadas espaciales (X,Y,Z),
tomando como referencia el punto de origen situado en (0,0,0). Cualquier elemento
virtual se modela mediante tringulos que forman la malla, y cada tringulo se dene
por medio de tres o mas ndices en una lista de puntos. Aqu se puede mostrar un
ejemplo de una malla que representa un cubo.
Para representar los tringulos del cubo siguiente, necesitaramos tres vrtices por
tringulo y tres coordenadas por cada uno de esos vrtices. Existen diferentes formas
de ahorrar espacio y optimizar la representacin de tringulos. Si se tiene en cuenta
que varios tringulos tienen vrtices en comn, no es necesario representar esos vrtices ms de una vez. El mtodo consiste en representar nicamente los tres ndices
del primer tringulo y, para el resto de tringulos, nicamente se aade el vrtice adicional. Esta tcnica sera similar a dibujar el cubo con un lpiz sin separar en ningn
momento la punta del lpiz del papel.
Listado 11.1: Representacin de los vrtices de un cubo con respecto al origen de coordenadas
1 Vec3 TestObject::g_SquashedCubeVerts[] =
2 {
3
Vec3( 0.5,0.5,-0.25),
// Vertex 0.
4
Vec3(-0.5,0.5,-0.25),
// Vertex 1.
5
Vec3(-0.5,0.5,0.5),
// And so on.
6
Vec3(0.75,0.5,0.5),
7
Vec3(0.75,-0.5,-0.5),
8
Vec3(-0.5,-0.5,-0.5),
9
Vec3(-0.5,-0.3,0.5),
10
Vec3(0.5,-0.3,0.5)
11 };
Con un simple cubo es complicado apreciar los benecios que se pueden obtener
con esta tcnica, pero si nos paramos a pensar que un modelo 3D puede tener miles
y miles de tringulos, la optimizacin es clara. De esta forma es posible almacenar n
tringulos con n+2 ndices, en lugar de n*3 vrtices como sucede en el primer caso.
[325]
Adems de la informacin relativa a la geometra del objeto, la mayora de formatos soporta la asociacin de informacin adicional sobre el material y textura de cada
uno de los polgonos que forman el objeto. El motor de rendering asumir, a menos
que se indique lo contrario, que cada grupo de tringulos tiene asociado el mismo material y las mismas texturas. El material dene el color de un objeto y como se reeja
la luz en l. El tamao destinado al almacenamiento de la informacin relativa al material puede variar dependiendo del motor de rendering empleado. Si al objeto no le
afecta la luz directamente y tiene un color slido, tan slo sern necesarios unos pocos
de bytes extra. Pero, por el contrario, si al objeto le afecta directamente la luz y ste
tiene una textura asociada, podra suponer casi 100 bytes ms por cada vrtice.
Datos de Animacin
Una animacin es en realidad la variacin de la posicin y la orientacin de los
vrtices que forman un objeto a lo largo del tiempo. Como se coment anteriormente,
una forma de representar una posicin o vrtice en el espacio es mediante tres valores reales asociados a las tres coordenadas espaciales X, Y y Z. Estos nmeros se
representan siguiendo el estndar IEEE (Institute of Electrical and Electronics Engineers)-754 para la representacin de nmeros reales en coma otante mediante 32 bits
(4 bytes). Por tanto, para representar una posicin 3D ser necesario emplear 12 bytes.
Adems de la posicin es necesario guardar informacin relativa a la orientacin,
y el tamao de las estructuras de datos empleadas para ello suele variar entre 12 y 16
bytes, dependiendo del motor de rendering. Existen diferentes formas de representar la
orientacin, el mtodo elegido inuir directamente en la cantidad de bytes necesarios.
Dos de los mtodos ms comunes en el desarrollo de videojuegos son los ngulos de
Euler y el uso de cuaterniones o tambin llamados cuaternios.
Para hacernos una idea del tamao que sera necesario en una sencilla animacin,
supongamos que la frecuencia de reproduccin de frames por segundo es de 25. Por
otro lado, si tenemos en cuenta que son necesarios 12 bytes por vrtice ms otros
12 (como mnimo) para almacenar la orientacin de cada vrtice, necesitaramos por
cada vrtice 12 + 12 = 24 bytes por cada frame. Si en un segundo se reproducen 25
frames, 25 x 24 = 600 bytes por cada vrtice y cada segundo. Ahora supongamos
que un objeto consta de 40 partes movibles (normalmente las partes movibles de un
personaje las determina el esqueleto y los huesos que lo forman). Si cada parte movible
necesita 600 bytes y existen 40 de ellas, se necesitara por cada segundo un total de
24.000 bytes.
Naturalmente 24.000 bytes puede ser un tamao excesivo para un solo segundo
y existen diferentes formas de optimizar el almacenamiento de los datos de animacin, sobre todo teniendo en cuenta que no todas las partes del objeto se mueven en
cada momento y, por tanto, no sera necesario almacenarlas de nuevo. Tampoco es
necesario almacenar la posicin de cada parte movible en cada uno de los 25 frames
que se reproducen en un segundo. Una solucin elegante es establecer frames claves
y calcular las posiciones intermedias de un vrtice desde un frame clave al siguiente
mediante interpolacin lineal. Es decir, no es necesario almacenar todas las posiciones
(nicamente la de los frames claves) ya que el resto pueden ser calculadas en tiempo
de ejecucin.
C11
De todo esto debemos aprender que la geometra de un objeto puede ocupar mucho menos espacio si se elige un formato adecuado para representar los tringulos
que forman el objeto. De cualquier forma, los requisitos de memoria se incrementan
notablemente cuando se emplean materiales complejos y se asocian texturas al objeto.
[326]
Otra posible mejora se podra obtener empleando un menor nmero de bytes para
representar la posicin de un vrtice. En muchas ocasiones, el desplazamiento o el
cambio de orientacin no son exageradamente elevados y no es necesario emplear 12
bytes. Si los valores no son excesivamente elevados se pueden emplear, por ejemplo,
nmeros enteros de 2 bytes. Estas tcnicas de compresin pueden reducir considerablemente el tamao en memoria necesario para almacenar los datos de animacin.
Mapas/Datos de Nivel
Cada uno de los niveles que forman parte de un juego tiene asociado diferentes
elementos como objetos estticos 3D, sonido de ambientacin, dilogos, entornos,
etc. Normalmente, todos estos contenidos son empaquetados en un chero binario
que suele ser de formato propietario. Una vez que el juego es instalado en nuestro
equipo es complicado acceder a estos contenidos de manera individual. En cambio,
durante el proceso de desarrollo estos datos se suelen almacenar en otros formatos
que s son accesibles por los miembros del equipo como, por ejemplo, en formato
XML. El formato XML es un formato accesible, interpretable y permite a personas que
trabajan con diferentes herramientas y en diferentes mbitos, disponer de un medio
para intercambiar informacin de forma sencilla.
Texturas
Hasta el momento, los datos descritos para el almacenamiento de la geometra de
los objetos, animacin y datos de nivel no suponen un porcentaje alto de la capacidad
de almacenamiento requerida. Las texturas (en tercer lugar), los cheros de audio y
vdeo son los que implican un mayor coste en trminos de memoria.
La textura es en realidad una imagen esttica que se utiliza como piel para cubrir
la supercie de un objeto virtual. Existen multitud de formatos que permiten obtener
imgenes de mayor o menor calidad y, en funcin de esta calidad, de un mayor o menor
tamao. Para obtener la mxima calidad, los diseadores grcos preeren formatos
no comprimidos de 32 bits como TIF (TIFF) o TGA. Por contra, el tamao de estas
imgenes podra ser excesivo e inuir en el tiempo de ejecucin de un videojuego. Por
ejemplo, una imagen RAW (imagen sin modicaciones, sin comprimir) de 32 bits con
una resolucin de 1024 x 768 pxeles podra alcanzar el tamao de 3MB. Por tanto, una
vez ms es necesario encontrar un equilibrio entre calidad y tamao. Cuando se disea
un videojuego, una de las principales dicultades que se plantean es la eleccin de un
formato adecuado para cada uno de los recursos, tal que se satisfagan las expectativas
de calidad y eciencia en tiempo de ejecucin.
Uno de los parmetros caractersticos de cualquier formato de imagen es la profundidad del color [62]. La profundidad del color determina la cantidad de bits empleados
para representar el color de un pxel en una imagen digital. Si con n bits se pueden
representar 2n valores, el uso de n bits por pxel ofrecer la posibilidad de representar
2n colores distintos, es decir, cuando mayor sea el nmero n de bits empleados, mayor
ser la paleta de colores disponibles.
32-bits (8888 RGBA (Red Green Blue Alpha)). Las imgenes se representan
mediante cuatro canales, R(red), G(green), B(blue), A(alpha), y por cada uno de
estos canales se emplean 8 bits. Es la forma menos compacta de representar un
mapa de bits y la que proporciona un mayor abanico de colores. Las imgenes
representadas de esta forma poseen una calidad alta, pero en muchas ocasiones
es innecesaria y otros formatos podran ser ms apropiados considerando que
hay que ahorrar el mayor nmero de recursos posibles.
Figura 11.2: Ejemplo de una imagen RGBA con porciones transparentes (canal alpha).
[327]
24-bits (888 RGB (Red Green Blue)). Este formato es similar al anterior pero
sin canal alpha, de esta forma se ahorran 8 bits y los 24 restantes se dividen en
partes iguales para los canales R(red), G(green) y B(blue). Este formato se suele
emplear en imgenes de fondo que contienen una gran cantidad de colores que
no podran ser representados con 16 u 8 bits.
24-bits (565 RGB, 8A). Este formato busca un equilibrio entre los dos anteriores. Permite almacenar imgenes con una profundidad de color aceptable y un
rango amplio de colores y, adems, proporciona un canal alfa que permite incluir porciones de imgenes traslcidas. El canal para el color verde tiene un bit
extra debido a que el ojo humano es ms sensible a los cambios con este color.
16-bits (555 RGB, 1 A). Similar al formato anterior, excepto el bit extra del
canal verde que, ahora, es destinado al canal alfa.
8-bits indexado. Este formato se suele emplear para representar iconos o imgenes que no necesitan alta calidad, debido a que la paleta o el rango de colores
empleado no es demasiado amplio. Sin embargo, son imgenes muy compactas
que ahorran mucho espacio en memoria y se transmiten o procesan rpidamente.
En este caso, al emplearse 8 bits, la paleta sera de 256 colores y se representa de forma matricial, donde cada color tiene asociado un ndice. Precisamente
los ndices son los elementos que permiten asociar el color de cada pxel en la
imagen a los colores representados en la paleta de colores.
11.1.3.
Casos de Estudio
Formato MD2/MD3
El formato MD2 es uno de los ms populares por su simplicidad, sencillez de uso
y versatilidad para la creacin de modelos (personajes, entornos, armas, efectos, etc).
Fue creado por la compaa id Software y empleado por primera vez en el videojuego
Quake II (ver Figura 11.5).
El formato MD2 representa la geometra de los objetos, texturas e informacin
sobre la animacin de los personajes en un orden muy concreto, tal como muestra la
Figura 11.6 [72]. El primer elemento en la mayora de formatos 3D es la cabecera. La
cabecera se sita al comienzo del chero y es realmente til ya que contiene informacin relevante sobre el propio chero sin la necesidad de tener que buscarla a travs
de la gran cantidad de datos que vienen a continuacin.
Cabecera
La cabecera del formato MD2 contiene los siguientes campos:
Listado 11.3: Cabecera del formato MD2
1 struct SMD2Header {
2
int m_iMagicNum;
3
int m_iVersion;
4
int m_iSkinWidthPx;
5
int m_iSkinHeightPx;
6
int m_iFrameSize;
7
int m_iNumSkins;
8
int m_iNumVertices;
9
int m_iNumTexCoords;
C11
[328]
Figura 11.5: Captura del vdeojuego Quake II, desarrollado por la empresa id Software, donde se emple
por primera vez el formato MD2
10
11
12
13
14
15
16
17
18
19
20 };
int
int
int
int
int
int
int
int
int
m_iNumTriangles;
m_iNumGLCommands;
m_iOffsetSkins;
m_iOffsetTexCoords;
m_iOffsetTriangles;
m_iOffsetFrames;
m_iOffsetGlCommands;
m_iFileSize;
m_iNumFrames;
El tamao total es de 68 bytes, debido a que cada uno de los enteros se representa haciendo uso de 4 bytes. A continuacin, en el siguiente listado se describir
brevemente el signicado de cada uno de estos campos [72].
En primer lugar podemos apreciar un campo que se reere al "nmero mgico".
Este nmero sirve para identicar el tipo de archivo y comprobar que en efecto
se trata de un chero con formato MD2.
Versin del chero. De esta forma se lleva a cabo un control de versiones y se
evita el uso de versiones obsoletas del chero.
La siguiente variable hace referencia a la textura que se utiliza para cubrir la
supercie del modelo. En este formato, cada modelo MD2 puede utilizar un
piel o textura en un instante concreto de tiempo, aunque se podran cargar varias
texturas e ir alternndolas a lo largo del tiempo.
m_iSkinWidthPx y m_iSkinHeightPx representan la anchura y altura de la textura en pxeles.
[329]
C11
[330]
Las cinco variables siguientes se emplean para denir el offset o direccin relativa
(el nmero de posiciones de memoria que se suman a una direccin base para obtener
la direccin absoluta) en bytes de cada una de las partes del chero. Esta informacin
es fundamental para facilitar los saltos en las distintas partes o secciones del chero
durante el proceso de carga. Por ltimo, la variable m_iFileSize representa el tamao
en bytes desde el inicio de la cabecera hasta el nal del chero.
Frames y Vrtices
A continuacin se muestra la estructura que representa la informacin que se maneja por frame en el formato MD2 [72]:
Listado 11.4: Informacin empleada en cada frame en el formato MD2
1 struct SMD2Frame {
2
float m_fScale[3];
3
float m_fTrans[3];
4
char m_caName[16];
5
SMD2Vert * m_pVertss
6
7
SMD2Frame()
8
{
9
m_pVerts = 0;
10
}
11
~SMD2Frame()
12
{
13
if(m_pVerts) delete [] m_pVerts;
14
}
15 };
Como se puede apreciar en la estructura anterior, cada frame comienza con seis
nmeros representados en punto otante que representan la escala y traslacin de los
vrtices en los ejes X, Y y Z. Posteriormente, se dene una cadena de 16 caracteres
que determinan el nombre del frame, que puede resultar de gran utilidad si se quiere
hacer referencia a ste en algn momento. Por ltimo se indica la posicin de todos los
vrtices en el frame (necesario para animar el modelo 3D). En cada uno de los frames
habr tantos vrtices como se indique en la variable de la cabecera m_iNumVerts. En la
ltima parte de la estructura se puede diferenciar un constructor y destructor, que son
necesarios para reservar y liberar memoria una vez que se ha reproducido el frame, ya
que el nmero mximo de vrtices que permite almacenar el formato MD2 por frame
es 2048.
Si se renderizaran nicamente los vrtices de un objeto, en pantalla tan slo se
veran un conjunto de puntos distribuidos en el espacio. El siguiente paso consistir
en unir estos puntos para denir los tringulos y dar forma al modelo.
Triangularizacin
En el formato MD2 los tringulos se representan siguiendo la tcnica descrita en
la Seccin 11.1.2. Cada tringulo tiene tres vrtices y varios tringulos tienen ndices
en comn [72]. No es necesario representar un vrtice ms de una vez si se representa
la relacin que existe entre ellos mediante el uso de ndices en una matriz.
Adems, cada tringulo debe tener asociado una textura, o sera ms correcto decir, una parte o fragmento de una imagen que representa una textura. Por tanto, es
necesario indicar de algn modo las coordenadas de la textura que corresponden con
cada tringulo. Para asociar las coordenadas de una textura a cada tringulo se emplean el mismo nmero de ndices, es decir, cada ndice de un vrtice en el tringulo
tiene asociado un ndice en el array de coordenadas de una textura. De esta forma se
puede texturizar cualquier objeto fcilmente.
[331]
A continuacin se muestra la estructura de datos que se poda emplear para representar los tringulos mediante asociacin de ndices en un array y la asociacin de
coordenadas de la textura para cada vrtice:
tura
Listado 11.5: Estructura de datos para representar los tringulos y las coordenadas de la tex-
1 struct SMD2Tri {
2
unsigned short m_sVertIndices[3];
3
unsigned short m_sTexIndices[3];
4 };
En el formato MD2 existen dos formas de asociar texturas a los objetos. La primera de ellas consiste en incluir el nombre de las texturas embebidos en el chero MD2.
El nmero de cheros de texturas y la localizacin, se encuentran en las variables de
la cabecera m_iNumSkins y m_iOffsetSkins. Cada nombre de textura ocupa 64 caracteres alfanumricos como mximo. A continuacin se muestra la estructura de datos
empleada para denir una textura [72]:
Listado 11.6: Estructura de datos para denir una textura embebida en un chero MD2
1 struct SMD2Skin
2 {
3
char m_caSkin[64];
4
CImage m_Image;
5 };
Listado 11.7: Estructura de datos para representar las coordenadas de una textura
1 struct SMD2TexCoord {
2
float m_fTex[2];
3 };
C11
Inclusin de Texturas
[332]
[333]
Ser un formato de cdigo libre, representado en XML para liberar los recursos
digitales de formatos binarios propietarios.
Proporcionar un formato estndar que pueda ser utilizado por una amplia mayora de herramientas de edicin de modelos tridimensionales.
Intentar que sea adoptado por una amplia comunidad de usuarios de contenidos
digitales.
Proveer un mecanismo sencillo de integracin, tal que toda la informacin posible se encuentre disponible en este formato.
Ser la base comn de todas las transferencias de datos entre aplicaciones 3D.
COLLADA Core Elements. Donde se dene la geometra de los objetos, informacin sobre la animacin, cmaras, luces, etc.
COLLADA Physics. En este apartado es posible asociar propiedades fsicas
a los objetos, con el objetivo de reproducir comportamientos lo ms realistas
posibles.
COLLADA FX. Se establecen las propiedades de los materiales asociados a
los objetos e informacin valiosa para el renderizado de la escena.
No es nuestro objetivo ver de forma detallada cada uno de estos tres bloques, pero
s se realizar una breve descripcin de los principales elementos del ncleo ya que
stos componen los elementos bsicos de una escena, los cuales son comunes en la
mayora de formatos.
COLLADA Core Elements
En este bloque se dene la escena (<scene>) donde transcurre una historia y los
objetos que participan en ella, mediante la denicin de la geometra y la animacin
de los mismos. Adems, se incluye informacin sobre las cmaras empleadas en la
escena y la iluminacin. Un documento con formato COLLADA slo puede contener
un nodo <scene>, por tanto, ser necesario elaborar varios documentos COLLADA
para denir escenas diferentes. La informacin de la escena se representa mediante un
grafo acclico y debe estar estructurado de la mejor manera posible para que el procesamiento sea ptimo. A continuacin se muestran los nodos/etiquetas relacionados
con la denicin de escenas [68].
Listado 11.8: Nodos empleados en COLLADA para la denicin de escenas
1
2
3
4
5
6
7
<instance_node>
<instance_visual_scene>
<library_nodes>
<library_visual_scenes>
<node>
<scene>
<visual_scene>
C11
[334]
<COLLADA>
<library_nodes id="comp">
<node name="earth">
</node>
<node name="sky">
</node>
</library_nodes>
<library_visual_scenes>
<visual_scene id="world">
<instance_library_nodes url="#comp">
</visual_scene>
</library_visual_scenes>
<scene>
<instance_visual_scene url="#world"/>
</scene>
</COLLADA>
<control_vertices>
<geometry>
<instance_geometry>
<library_geometries>
<lines>
<linestrips>
<mesh>
<polygons>
<polylist>
<spline>
<triangles>
<trifans>
[335]
2
<source id="position" />
3
<source id="normal" />
4
<vertices id="verts">
5
<input semantic="POSITION" source="#position"/>
6
</vertices>
7
<polygons count="1" material="Bricks">
8
<input semantic="VERTEX" source="#verts" offset="0"/>
9
<input semantic="NORMAL" source="#normal" offset="1"/>
10
<p>0 0 2 1 3 2 1 3</p>
11
</polygons>
12 </mesh>
Luces puntuales
Luces direccionales
Puntos de luz
y se utilizan los siguientes nodos o etiquetas:
Listado 11.12: Nodos empleados en COLLADA para la denicin de fuentes de luz
1
2
3
4
5
6
7
8
<ambient>
<color>
<directional>
<instance_light>
<library_lights>
<Light>
<point>
<spot>
Por otro lado, COLLADA tambin permite la denicin de cmaras. Una cmara
declara una vista de la jerarqua del grafo de la escena y contiene informacin sobre la
ptica (perspectiva u ortogrca). Los nodos que se utilizan para denir una cmara
son los siguientes [68]:
Listado 11.13: Nodos empleados en COLLADA para denir cmaras
1
2
3
4
5
6
7
<camera>
<imager>
<instance_camera>
<library_cameras>
<optics>
<orthographic>
<Perspective>
C11
Luces ambientales
[336]
7
</aspect_ratio>
8
<znear>1.0</znear>
9
<zfar>1000.0</zfar>
10
</perspective>
11
</technique_common>
12
</optics>
13 </camera>
[337]
Adems es necesario saber la informacin geomtrica de cada uno de los vrtices, es decir, posicin, normal, coordenada de textura y el nmero de vrtices de la
submalla
Listado 11.16: Geometra de un Objeto en OgreXML
1 <geometry vertexcount="3361">
2
<vertexbuffer positions="true" normals="true" texture_coords="1">
3
<vertex>
4
<position x="-0.000000" y="0.170180" z="-0.000000"/>
5
<normal x="0.000000" y="1.000000" z="0.000000"/>
6
<texcoord u="0.000000" v="1.000000"/>
7
</vertex>
1.000000"/>
3
.............................
4 </boneassignments>
5 </submesh>
/>
4
<boneparent bone="rota_ceja_derecha" parent="ceja_derecha" />
5
<boneparent bone="pecho" parent="torso" />
6
<boneparent bone="hombro.r" parent="pecho" />
7
.............................
8 </bonehierarchy>
C11
Para la asignacin de los huesos se indican los huesos y sus pesos. Como se puede
observar, el ndice utilizado para los huesos empieza por el 0.
[338]
Por ltimo, si se han creado animaciones asociadas al esqueleto y han sido exportadas, se mostrar cada una de ellas identicndolas con el nombre denido previamente
en Blender y la longitud de la accin medida en segundos. Cada animacin contendr
informacin sobre los huesos que intervienen en el movimiento (traslacin, rotacin y
escalado) y el instante de tiempo en el que se ha denido el frame clave:
Listado 11.20: Animacin de los huesos de un esqueleto en OgreXML
1 <animations>
2
<animation name="ascensor" length="2.160000">
3
<track bone="d1root.l">
4
<keyframes>
5
<keyframe time="0.000000">
6
<translate x="-0.000000" y="0.000000" z="0.000000"/>
7
<rotate angle="0.000000">
8
<axis x="-0.412549" y="0.655310" z="0.632749"/>
9
</rotate>
10
<scale x="1.000000" y="1.000000" z="1.000000"/>
11
</keyframe>
12
13
.............................
14
<keyframe time="2.160000">
15
<translate x="0.000000" y="-0.000000" z="0.000000"/>
16
<rotate angle="0.000000">
17
<axis x="-0.891108" y="0.199133" z="0.407765"/>
18
</rotate>
19
<scale x="1.000000" y="1.000000" z="1.000000"/>
20
</keyframe>
21
</keyframes>
22
.............................
23 </track>
Otros Formatos
A continuacin se incluye una tabla con alguno de los formatos ms comunes para
la representacin de objetos 3D, su extensin, editores que lo utilizan y un enlace
donde se puede obtener ms informacin sobre el formato (ver Tabla 3.1) [72].
11.2.
11.2.1.
ASC
B3D
BDF
BLEND
CAR
COB
DMO
DXF
HRC
INC
KF2
KFS
LWO
MB
MAX
MS3D
OBJ
PZ3
RAW
RDS
RIB
VRLM
X
XGL
EDITOR
3D Meta File, QuickDraw3D
Jedi Knight
3D Studio Max, formato binario
Motor Genesis 3D
3D Studio Max: versin de 3DS basada en texto
3D Studio Max: mnima representacin de datos representada en ASCII
Bryce 3D
Okino
Blender
Carrara
Calgari TrueSpace
Duke Nukem 3D
Autodesk Autocad
Softimage 3D
POV-RAY
Animaciones y poses en Max Payne
Max Payne: informacin de la malla y los materiales
Lightwave
Maya
3D Studio Max
Milkshape 3D
Alias|Wavefront
Poser
Tringulos RAW
Ray Dream Studio
Renderman File
Lenguaje para el modelado de realidad virtual
Formato Microsoft Direct X
Formato que utilizan varios programas CAD
Link
http://www.apple.com
http://www.lucasarts.com
http://www.discreet.com
http://www.genesis3D.com
http://www.discreet.com
http://www.discreet.com
http://www.corel.com
http://www.okino.com
http://www.blender.com
http://www.eovia.com/carrara
http://www.calgari.com
http://www.3drealms.com
http://www.autodesk.com
http://www.softimage.com
http://www.povray.org
http://www.maxpayne.com
http://www.maxpayne.com
http://www.newtek.com
http://www.aliaswavefront.com
http://www.discreet.com
http://www.swissquake.ch
http://aliaswavefront.com
http://www.curioslab.com
http://
http://www.metacreations.com
http://www.renderman.com
http://www.web3d.org
http://www.microsoft.com
http://www.xglspec.com
ALT + U . En la ventana para la conguracin de las preferencias del usuario pulsamos sobre el botn addons y en la parte inferior de la ventana pulsamos el botn install
addons. A continuacin el asistente nos ofrecer la posibilidad de elegir el script de
python con el exportador de Blender a Ogre. Finalmente, el exportador aparecer en
el listado de la derecha y marcaremos la casilla de vericacin.
Para comprobar que la instalacin es correcta, nos dirigimos al men File >Export
y comprobamos que entre las opciones aparece Ogre3D.
C11
EXT.
3DMF
3DO
3DS
ACT
ASE
[339]
[340]
11.2.2.
El objetivo es modelar una caja o cubo y aplicar una textura a cada una de las seis
caras. Cuando ejecutamos Blender aparece en la escena un cubo creado por defecto,
por tanto, no ser necesario crear ningn modelo adicional para completar este ejercicio. En el caso de que quisiramos aadir algn cubo ms sera sencillo, bastara con
dirigirnos al men Add >Mesh >Cube y aparecera un nuevo cubo en la escena.
Adems del cubo, Blender crea por defecto una fuente de luz y una cmara. Al
igual que sucede con los objetos, es posible aadir fuentes de luz adicionales y nuevas
cmaras. Para este ejercicio ser suciente con los elementos creados por defecto.
11.2.3.
Cada uno de los recuadros corresponde a la textura que se aplicar a una de las
caras del cubo en el eje indicado. En la Figura 11.11 se muestra la textura real que se
aplicar al modelo; tal como se puede apreciar est organizada de la misma forma que
se presenta en la Figura 11.10 (cargar la imagen llamada textura512.jpg que se aporta
con el material del curso). Todas las texturas se encuentran en una misma imagen
cuya resolucin es 512x512. Los mdulos de memoria de las tarjetas grcas estn
[341]
C11
Figura 11.11: Imagen con resolucin 512x512 que contiene las texturas que sern aplicadas a un cubo
Una vez conocida la textura a aplicar sobre el cubo y cmo est estructurada, se
describirn los pasos necesarios para aplicar las texturas en las caras del cubo mediante
la tcnica UV Mapping. En primer lugar ejecutamos Blender y dividimos la pantalla
en dos partes. Para ello situamos el puntero del ratn sobre el marco superior hasta
que el cursor cambie de forma a una echa doblemente punteada, pulsamos el botn
derecho y elegimos en el men otante la opcin Split Area.
En el marco de la derecha pulsamos el men desplegable situado en la esquina
inferior izquierda y elegimos la opcin UV/Image Editor. A continuacin cargamos la
imagen de la textura (textura512.jpg); para ello pulsamos en el men Image >Open
y elegimos la imagen que contiene las texturas. Despus de realizar estos datos la
apariencia del editor de Blender debe ser similar a la que aparece en la Figura 11.12
En el marco de la izquierda se pueden visualizar las lneas que delimitan el cubo,
coloreadas de color rosa, donde el modo de vista por defecto es Wireframe. Para visualizar por pantalla las texturas en todo momento elegimos el modo de vista Textured
en el men situado en el marco
Viewport Shading. Otra posibilidad consiste
inferior
[342]
Figura 11.12: Editor de Blender con una divisin en dos marcos. En la parte izquierda aparece la gura geomtrica a texturizar y en la parte derecha las texturas que sern aplicadas mediante la tcnica de
UV/Mapping
Por otro lado, el cubo situado en la escena tiene asociado un material por defecto,
pero ste no tiene asignada ninguna textura (en el caso de que no tenga un material
asociado, ser necesario crear uno). Para ello, nos dirigimos al men de materiales
situado en la barra de herramientas de la derecha (ver Figura 11.13).
tecla TAB para visualizar los vrtices y nos aseguramos que todos estn seleccionados
(todos deben estar coloreados de amarillo). Para seleccionar
o eliminar la seleccin
[343]
el cuadrado con la tecla S y para desplazarlo la tecla G (para hacer zoom sobre la
textura se gira la rueda central del ratn). Un segunda opcin es ajustar cada vrtice
de forma individual,
para ello se selecciona un vrtice con el botn derecho del ratn, pulsamos G y desplazamos el vrtice a la posicin deseada. Las dos alternativas
descritas anteriormente no son excluyentes y se pueden combinar, es decir, se podra
escalar el recuadro en primer lugar y luego ajustar los vrtices, e incluso escalar de
nuevo si fuera necesario.
Los pasos que se han realizo para establecer la textura de la parte superior del cubo
deben
ser repetidos para el resto de caras del cubo. Si renderizamos la escena pulsando
F12
11.2.4.
C11
Pulsamos A para que ninguno de los vrtices quede seleccionado y, de forma
manual, seleccionamos los vrtices de la cara superior (z+). Para seleccionar los vrtices
dos alternativas; la primera es seleccionar uno a uno manteniendo la tecla
existen
SHIFT pulsada y presionar el botn derecho del ratn en cada uno de los vrtices.
Una segunda opcin es dibujar
un rea que encuadre los vrtices, para ello hay que
pulsar primero la tecla B (Pulsa el botn intermedio del ratn y muvelo para cambiar
la perspectiva y poder asegurar as que se han elegido los vrtices correctos).
[344]
[345]
11.2.5.
Para cargar el cubo exportado en Ogre vamos a partir de ejemplo visto al comienzo
del curso Hello World. Recordemos que la estructura de directorios para la aplicacin
en Ogre era la siguiente:
Include
Media
Obj
Plugins
src
Una vez que se ha exportado el cubo con las texturas asociadas y se han generado los cheros Cube.mesh, Cube.mesh.xml y Scene.material, los incluimos en el
directorio media (tambin incluimos la textura "textura512.jpg"). Los cheros deben
incluirse en este directorio porque as est congurado en el chero resources.cfg, si
deseramos incluir los recursos en otro directorio deberamos variar la conguracin
en dicho chero.
En segundo lugar, cambiamos el nombre del ejecutable; para ello, abrimos el archivo makele y en lugar de EXEC := helloWorld, ponemos EXEC:= cubo. Despus
es necesario cambiar el cdigo de ejemplo para que cargue nuestro modelo. Abrimos
el archivo /src/main.cpp que se encuentra dentro en el directorio /src. El cdigo es el
siguiente:
Listado 11.21: Cdigo incluido en el archivo main.cpp
1 #include <ExampleApplication.h>
2 class SimpleExample : public ExampleApplication {
3
public : void createScene() {
4
Ogre::Entity *ent = mSceneMgr->createEntity("Sinbad", "
5
6
7 };
8
9 int
10
11
12
13 }
Sinbad.mesh");
mSceneMgr->getRootSceneNode()->attachObject(ent);
main(void) {
SimpleExample example;
example.go();
return 0;
C11
Directorios:
[346]
En nuestro caso la entidad a crear es aquella cuya malla se llama Cube.Mesh, sta
ya contiene informacin sobre la geometra de la malla y las coordenadas UV en el
caso de utilizacin de texturas.
Por lo tanto el cdigo tras la modicacin de la funcin createScene, sera el siguiente:
Listado 11.22: Modicacin del chero main.cpp para cargar el cubo modelado en Blender
1 public : void createScene() {
2
Ogre::Entity *ent = mSceneMgr->createEntity("Caja", "cubo.mesh"
3
4 }
);
mSceneMgr->getRootSceneNode()->attachObject(ent);
Al compilar (make) y ejecutar (./Cubo) aceptamos las opciones que vienen por
defecto y se puede observar el cubo pero muy lejano. A partir de ahora todas las
operaciones que queramos hacer (translacin, escalado, rotacin, ...) tendr que ser
a travs de cdigo. A continuacin se realiza una operacin de traslacin para acercar
el cubo a la cmara:
Listado 11.23: Traslacin del cubo para acercarlo hacia la cmara
1 Entity *ent1 = mSceneMgr->createEntity( "Cubo", "Cube.mesh" );
2 SceneNode *node1 = mSceneMgr->getRootSceneNode()->
createChildSceneNode( );
3 node1->attachObject( ent1 );
4 node1->translate(Vector3(0, 0, 490 ));
11.3.
[347]
Antes de continuar, el lector podra (opcionalmente) completar el estudio del captulo 13, ya que utilizaremos como base el cdigo fuente obtenido en el ltimo ejemplo
de dicho captulo, y estudiaremos algunas cuestiones referentes al gestor de recursos.
Blender, a diferencia de los sistemas de CAD (Computer Aided Design) (que emplean modelos de CSG (Constructive Solid Geometry), es una herramienta de modelado de contorno B-R EP (Boundary Representation). Esto implica que, a diferencia
de los sistemas basados en CSG, trabaja con modelos huecos denidos por vrtices,
aristas y caras. Como hemos estudiado en el captulo de introduccin matemtica, las
coordenadas de estos modelos se especican de forma relativa a su centro, que dene
el origen de su sistema de referencia local.
Este centro dene el sistema de referencia local, por lo que resulta crtico poder
cambiar el centro del objeto ya que, tras su exportacin, se dibujar a partir de esta
posicin.
Con el objeto seleccionado en Modo de Objeto se puede indicar a Blender que
recalcule el centro geomtrico
del objeto en el panel de Object Tools (accesible con
la tecla T ), en el botn Origin (ver Figura 11.20). Una vez pulsado Origin, podemos
elegir entre Geometry to Origin, que desplaza los vrtices del objeto de modo que
se ajustan a la posicin jada del centro geomtrico, Origin to Geometry que realiza
la misma operacin de alineacin, pero desplazando el centro en lugar de mover los
vrtices, o Origin to 3D Cursor que cambia la posicin del centro del objeto a la
localizacin actual del cursor 3D.
Figura 11.21: Propiedades del panel Transform en modo edicin (tecla T). Permiten indicar las coordenadas de cada vrtice con respecto
del Sistema de Referencia Local y
del Sistema de Referencia Universal (Global).
C11
En Blender el centro del objeto se representa mediante un punto de color rosa (ver
Figura 11.19). Si tenemos activos los manejadores en la cabecera de la ventana 3D
, ser el punto de donde comienzan estos elementos de la interfaz.
[348]
b)
a)
c)
d)
Figura 11.22: Aplicacin de transformaciones geomtricas en modo Edicin. a) Situacin inicial del modelo al que le aplicaremos un escalado de 0.5 respecto del eje Z. b) El escalado se aplica en modo Edicin.
El campo Vertex Z muestra el valor correcto. c) Si el escalado se aplic en modo objeto, el campo Vertex Z
no reeja la geometra real del objeto. Esto puede comprobarse fcilmente si alguno de los campos Scale
no son igual a uno, como en d).
Otra operacin muy interesante consiste en posicionar un objeto con total precisin. Para realizar esta operacin puede ser suciente con situarlo numricamente,
comohemos
visto anteriormente, accediendo al panel de transformacin mediante la
tecla N . Sin embargo, otras veces necesitamos situarlo en una posicin relativa a otro
objeto; necesitaremos situarlo empleando el cursor 3D.
objeto que haya sido seleccionado en modo objeto) y mediante Shift S Selection to
Cursor moveremos el objeto seleccionado a la posicin del puntero 3D (ver Figura
11.24). Mediante este sencillo mecanismo de 2 pasos podemos situar los objetos con
precisin, y modicar el centro del objeto a cualquier punto de inters.
11.3.1.
Ejemplo de uso
b)
c)
d)
C11
a)
[349]
e)
f)
Figura 11.24: Ejemplo de uso del operador Cursor to Selected y Selection to Cursor. a) Posicin inicial de
los dos modelos. Queremos llegar a la posicin f). En modo edicin, elegimos un vrtice superior del cubo
y posicionamos el puntero 3D ah (empleando Shift S Cursor to Selected). c) A continuacin modicamos
el valor de X e Y de la posicin del cursor numricamente, para que se site en el centro de la cara. d)
Elegimos Origin to 3D Cursor entre las opciones disponibles en el botn Origin del panel Object Tools.
Ahora el cubo tiene su centro en el punto medio de la cara superior. e) Elegimos el vrtice sealado en la
cabeza del mono y posicionamos ah el puntero 3D. Finalmente en f) cambiamos la posicin del cubo de
forma que interseca exactamente en ese vrtice (Shift S/ Selection to Cursor).
Como hemos visto, la eleccin correcta del centro del objeto facilita el cdigo en
etapas posteriores. Si el modelo est normalizado (con escala 1.0 en todos los ejes),
las coordenadas del espacio 3D de Blender pueden ser fcilmente transformadas a
coordenadas de Ogre. En este caso, vamos a situar el centro de cada objeto de la
escena de forma que est situado
en el Z = 0. Para ello, con cada objeto
exactamente
[350]
int MyApp::start() {
// ... Carga de configuracion, creacion de window ...
Ogre::Camera* cam = _sceneManager->createCamera("MainCamera");
cam->setPosition(Ogre::Vector3(5,20,20));
cam->lookAt(Ogre::Vector3(0,0,0));
cam->setNearClipDistance(5);
cam->setFarClipDistance(100);
// ... Viewport, y createScene...
}
void MyApp::createScene() {
Ogre::Entity* ent1 = _sceneManager->createEntity("MS.mesh");
Ogre::SceneNode* node1 = _sceneManager->createSceneNode("MS");
node1->attachObject(ent1);
node1->translate(0,2,0);
_sceneManager->getRootSceneNode()->addChild(node1);
Ogre::Entity* ent2 = _sceneManager->createEntity("Mando.mesh");
Ogre::SceneNode* node2 = _sceneManager->createSceneNode("Mando");
node2->attachObject(ent2);
node2->translate(0,2,0);
node1->addChild(node2);
// ... creacion del plano, luces, etc...
}
Figura 11.27: Posicionamiento del puntero 3D en relacin al centro del objeto. En muchas situaciones
puede ser conveniente especicar que varios objetos tengan el centro en una misma posicin del espacio
(como veremos en la siguiente seccin). Esto puede facilitar la construccin de la aplicacin.
Automatzate!!
Obviamente, el proceso de exportacin de los datos relativos al posicionamiento de objetos en la escena (junto con otros datos de inters) debern ser automatizados deniendo un formato de escena para Ogre. Este formato tendr la informacin necesaria para cada juego particular.
1
2
3
4
5
6
7
8
9
10
Por ltimo, sera deseable poder exportar la posicin de las cmara para percibir
la escena con la misma conguracin que en Blender. Existen varias formas de congurar el camera pose. Una de las ms cmodas para el diseador es trabajar con la
posicin de la cmara y el punto al que sta mira. Esta especicacin es equivalente a
indicar la posicin
yel punto look at (en el primer fragmento de cdigo de la seccin,
en las lneas 4 y 5 .
Para que la cmara apunte hacia un objeto de la escena, puede crearse una restriccin de tipo Track To. Para ello,
un objeto vaco a la escena (que no tiene
aadimos
Shift A Add / Empty. Ahora seleccionamos primero la crepresentacin), mediante
mara, y luego con Shift pulsado seleccionamos el Empty y pulsamos Control T Track
To Constraint. De este modo, si desplazamos la cmara, obtendremos que siempre
est mirando hacia el objeto Empty creado (ver Figura 11.29).
Cuando tengamos la fotografa de la escena montada, bastar con consultar el
valor de posicin de la cmara y el Empty, y asignar esos valores al cdigo de posicionamiento y look at de la misma (teniendo en cuenta las diferencias entre sistemas
de coordenadas de Blender y Ogre). El resultado se encuentra resumido en la Figura
11.30.
11.4.
En esta seccin estudiaremos un ejemplo que cubre varios aspectos que no han sido tratados en el documento, relativos al uso del gestor de recursos y de escena. Desarrollaremos un demostrador de 3D Picking que gestionar manualmente el puntero del
ratn (mediante overlays), gestionar recursos empaquetados en archivos .zip (empleando las facilidades que proporciona Ogre), cargar geometra esttica y utilizar
el potente sistema de queries del SceneManager con mscaras.
C11
[351]
[352]
cam->setPosition (Vector3(5.7,6.3,7.1));
cam->lookAt (Vector3(0.9,0.24,1.1));
cam->setFOVy (Ogre::Degree(52));
Figura 11.30: Resultado de aplicar la misma cmara en Blender y Ogre. El campo de visin de la lente
puede consultarse en las propiedades especcas de la cmara, indicando que el valor quiere representarse
en grados (ver lista desplegable de la derecha)
11.4.1.
Recursos empaquetados
El gestor de recursos de Ogre permite utilizar cheros .zip deniendo en el archivo de conguracin de recursos que el tipo es Zip.
De este modo, el archivo de conguracin de recursos asociado a este ejemplo se
muestra en la Figura 11.31. La implementacin del cargador de recursos que estudiamos en el captulo 13 permite su utilizacin directamente.
11.4.2.
[353]
Figura 11.33: Ejemplo de aplicacin que desarrollaremos en esta seccin. El interfaz permite seleccionar
objetos en el espacio 3D y modicar algunas de sus propiedades (escala y rotacin).
El desplazamiento
de la rueda del ratn se obtiene en la coordenada Z con getMou
seState (lnea 5 ). Utilizamos esta informacin para desplazar la cmara relativamente
en su eje local Z.
Listado 11.28: Fragmentos de MyFrameListener.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
11.4.3.
Geometra Esttica
Sobre eciencia...
Realizar 100 llamadas de 10.000
polgonos a la GPU puede ser un
orden de magnitud menos eciente
que realizar 10 llamadas de 100.000
polgonos.
La geometra esttica, como su nombre indica, est pensada para elementos grcos que no modican su posicin durante la ejecucin de la aplicacin. Est pensada
para enviar pocos paquetes (lotes) grandes de geometra a la GPU en lugar de muchos
pequeos. Esto permite optimizar el rendimiento en su posterior despliegue. De este
modo, a menor nmero de paquetes enviados a la GPU tendremos mayor rendimiento
en la aplicacin.
El uso de la memoria empleando geometra esttica sin embargo es mayor. Mientras que los MovableObject comparten mallas poligonales (siempre que sean instancias del mismo objeto), empleando geometra esttica se copian los datos para cada
instancia de la geometra.
C11
[354]
11.4.4.
Queries
[355]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Cabe destacar el establecimiento de ags propios (en las lneas 5 y 19 ) que
estudiaremos
en la seccin 11.4.5,
ascomo la propiedad de visibilidad del nodo (en
7 y 21 ). El bucle denido en 13-23 sirve para cargar las cajas lmite mostradas en
la Figura 11.35.
Las preguntas que pueden realizarse al gestor de escena se dividen en cuatro categoras principales: 1. Caja lmite, denida mediante dos vrtices opuestos, 2. Esfera,
denida por una posicin y un radio, 3. Volumen, denido por un conjunto de tres o
ms planos y 4. Rayo, denido por un punto (origen) y una direccin.
Otras intersecciones
Ogre soporta igualmente realizar
consultas sobre cualquier tipo de intersecciones arbitrarias entre objetos de la escena.
Primera
interseccin
rayMouse
origen
C11
[356]
ox4
Col_B
Col_
Box1
Col_
Box5
Box2
Col_
Col_
Box3
o
Suel
Col_
Figura 11.35: A la izquierda la geometra utilizada para las preguntas al gestor de escena. A la derecha la
geometra esttica. El centro de todas las entidades de la izquierda coinciden con el centro del modelo de
la derecha. Aunque en la imagen los modelos estn desplazados con respecto del eje X, en realidad ambos
estn exactamente en la misma posicin del mundo.
La creacin de las Queries son costosas computacionalmente (por lo que interesa realizarlas al inicio; en los constructores). Sin embargo su ejecucin
puede realizarse sin problema en el bucle principal de dibujado. El rendimiento se ver notablemente afectado si creas y destruyes la query en cada
frame.
Figura 11.37: En el caso de objetos mviles, Ogre emplea una caja lmite (denida por las coordenadas mayor y menor de los vrtices
del modelo) para calcular la interseccin rayo-objeto y optimizar as
los clculos. Esto implica que, en el
caso del test de interseccin de la gura dara que hay colisin entre el
rayo y el objeto. No obstante, es posible realizar manualmente un test
de colisin con precisin (a nivel de
polgono) si el juego lo requiere.
[357]
11.4.5.
Mscaras
C11
[358]
Captulo
12
APIS de Grcos 3D
Carlos Gonzlez Morcillo
12.1.
Introduccin
[360]
En este breve resumen de OpenGL se dejarn gran cantidad de aspectos sin mencionar. Se recomienda el estudio de la gua ocial de OpenGL [86] (tambin conocido
como El Libro Rojo de OpenGL para profundizar en esta potente biblioteca grca.
Otra fantstica fuente de informacin, con ediciones anteriores del Libro Rojo es la
pgina ocial de la biblioteca1 .
El propio nombre OpenGL indica que es una Biblioteca para Grcos Abierta2 .
Una de las caractersticas que ha hecho de OpenGL una biblioteca tan famosa es que es
independiente de la plataforma sobre la que se est ejecutando (en trminos software
y hardware). Esto implica que alguien tendr que encargarse de abrir una ventana
grca sobre la que OpenGL pueda dibujar los preciosos grcos 3D.
12.2.
Modelo Conceptual
Las llamadas a funciones de OpenGL estn diseadas para aceptar diversos tipos
de datos como entrada. El nombre de la funcin identica adems los argumentos que
recibir. Por ejemplo, en la Figura 12.4 se llama a una funcin para especicar un
nuevo vrtice en coordenadas homogneas (4 parmetros), con tipo de datos double
y en formato vector. Se denen tipos enumerados como redenicin de tipos bsicos
(como GLoat, GLint, etc) para facilitar la compatibilidad con otras plataformas. Es
buena prctica utilizar estos tipos redenidos si se planea compilar la aplicacin en
otros sistemas.
El Modelo Conceptual General de OpenGL dene dos operaciones bsicas que
el programador puede realizar en cada instante; 1) dibujar algn elemento o 2) cambiar
el estado de cmo se dibujan los elementos. La primera operacin de dibujar algn
elemento tiene que ser a) una primitiva geomtrica (puntos, lneas o polgonos) o b)
una primitiva de imagen.
1 http://www.opengl.org/
2 Las
GLUT (FreeGLUT)
Programa de Usuario
Widget Opengl
GTKGLExt, Motif, etc...
GLU
GL
Otras Bibliotecas
Software
Modelo Conceptual
de OpenGL
Dibujar
Primitiva
Geomtrica
Cambiar
Estado
Primitiva
Imagen
3 http://www.libsdl.org/
[361]
glVertex4dv (v);
Componentes
Tipo de Datos
2 (x,y)
3 (x, y, z)
4 (x, y, z, h)
Vector (o escalar)
v implica vector. Si no
aparece, los parmetros
son escalares.
12.2.1.
Cambio de Estado
A modo de curiosidad: OpenGL
cuenta con ms de 400 llamadas a
funcin que tienen que ver con el
cambio del estado interno de la biblioteca.
Cambio de Estado
4 Como veremos a continuacin, los colores en OpenGL se especican en punto otante con valores
entre 0 y 1. Las primeras tres componentes se corresponden con los canales RGB, y la cuarta es el valor de
transparencia Alpha.
C12
[362]
12.2.2.
Dibujar Primitivas
La operacin de Dibujar Primitivas requiere habitualmente que stas se denan en coordenadas homogneas. Todas las primitivas geomtricas se especican con
vrtices. El tipo de primitiva determina cmo se combinarn los vrtices para formar la superce poligional nal. La creacin de primitivas se realiza entre llamadas a
glBegin(PRIMITIVA) y glEnd(), siendo PRIMITIVA alguna de las 10 primitivas
bsicas soportadas por OpenGL5 . No obstante, el mdulo GLU permite dibujar otras
supercies ms complejas (como cilindros, esferas, discos...), y con GLUT es posible
dibujar algunos objetos simples. Como se puede ver, OpenGL no dispone de funciones para la carga de modelos poligonales creados con otras aplicaciones (como por
ejemplo, en formato OBJ o MD3). Es responsabilidad del programador realizar esta
carga y dibujarlos empleando las primitivas bsicas anteriores.
Una vez estudiados los cambios de estado y el dibujado de primitivas bsicas,
podemos especicar en la funcin display del siguiente listado para que dibuje un
cuadrado (primitiva GL_QUADS). Las lneas relativas al despliegue del cuadrado se
corresponden con el intervalo 7-17 . Especicamos la posicin de cada vrtice del
cuadrado modicando el color (el estado interno). OpenGL se encargar de calcular
la transicin de color entre cada punto intermedio del cuadrado.
Listado 12.1: Ejemplo de cambio de estado y dibujo de primitivas.
1 void display() {
2
glClear( GL_COLOR_BUFFER_BIT );
3
glEnable(GL_DEPTH_TEST);
4
glLoadIdentity();
/* Cargamos la matriz identidad
5
glTranslatef( 0.f, 0.f, -4.f );
6
7
glBegin(GL_QUADS);
/* Dibujamos un cuadrado
8
glColor3f(1.0, 0.0, 0.0);
/* de dos unidades de lado.
9
glVertex3f(1.0, 1.0, 0.0);
/* Especificamos la coorde10
glColor3f(0.0, 1.0, 0.0);
/* nada 3D de cada vertice
11
glVertex3f(1.0, -1.0, 0.0); /* y su color asociado.
12
glColor3f(0.0, 0.0, 1.0);
13
glVertex3f(-1.0, -1.0, 0.0);
14
glColor3f(1.0, 1.0, 1.0);
15
glVertex3f(-1.0, 1.0, 0.0);
16
glEnd();
17
glDisable(GL_DEPTH_TEST);
18
19
glutSwapBuffers();
20 }
12.3.
*/
*/
*/
*/
*/
*/
Pipeline de OpenGL
Como vimos en el captulo 8.2, los elementos de la escena sufren diferentes transformaciones en el pipeline de grcos 3D. Algunas de las principales etapas se corresponden con las siguiente operaciones:
Transformacin de Modelado: En la que los modelos se posicionan en la escena y se obtienen las Coordenadas Universales.
5 Las 10 primitivas bsicas de OpenGL son GL_POINTS, GL_LINE_STRIP, GL_LINES,
GL_LINE_LOOP,
GL_POLYGON,
GL_TRIANGLE_STRIP,
GL_TRIANGLES,
GL_TRIANGLE_FAN, GL_QUADS y GL_QUAD_STRIP
[363]
Transformacin de Visualizacin: Donde se especica la posicin de la cmara y se mueven los objetos desde las coordenadas del mundo a las Coordenadas
de Visualizacin (o coordenadas de cmara).
Transformacin de Proyeccin: Obteniendo Coordenadas Normalizadas en el
cubo unitario.
Transformacin de Recorte y de Pantalla: Donde se obtienen, tras el recorte
(o clipping de la geometra), las coordenadas 2D de la ventana en pantalla.
+X
+Z
-X
12.3.1.
-Y
Transformacin de Visualizacin
Matriz Identidad
La Matriz Identidad es una matriz
4x4 con valor 1 en la diagonal principal, y 0 en el resto de elementos.
La multiplicacin de esta matriz I
por una matriz M cualquiera siempre obtiene como resultado M . Esto
es necesario ya que OpenGL siempre multiplica las matrices que se le
indican para modicar su estado interno.
center
(cx,cy,cz)
up
(ux,uy,uz)
eye
(ex,ey,ez)
12.3.2.
Transformacin de Modelado
Las transformaciones de modelado nos permiten modicar los objetos de la escena. Existen tres operaciones bsicas de modelado que implementa OpenGL con
llamadas a funciones. No obstante, puede especicarse cualquier operacin aplicando
una matriz denida por el usuario.
Traslacin. El objeto se mueve a lo largo de un vector. Esta operacin se realiza
mediante la llamada a glTranslate(x,y,z).
C12
+Y
-Z
[364]
12.3.3.
Transformacin de Proyeccin
distancias de los planos de recorte (igual que los parmetros f y n del frustum. fov
especica en grados sexagesimales el ngulo en el eje Y de la escena que es visible
para el usuario, y aspect indica la relacin de aspecto de la pantalla (ancho/alto).
12.3.4.
Matrices
OpenGL utiliza matrices 4x4 para representar todas sus transformaciones geomtricas. Las matrices emplean coordenadas homogneas, como se estudi en la seccin 9.2.1. A diferencia de la notacin matemtica estndar, OpenGL especica por
defecto las matrices por columnas, por lo que si queremos cargar nuestras propias
matrices de transformacin debemos tener en cuenta que el orden de elementos en
OpenGL es el siguiente:
m0 m4 m8 m12
m1 m5 m9 m13
(12.1)
m m m
m14
2
6
10
m3 m7 m11 m15
OpenGL internamente maneja pilas de matrices, de forma que nicamente la matriz de la cima de cada pila es la que se est utilizando en un momento determinado.
Hay cuatro pilas de matrices en OpenGL:
1. Pila Modelview (GL_MODELVIEW). Esta pila contiene las matrices de Transformacin de modelado y visualizacin. Tiene un tamao mnimo de 32 matrices
(aunque, dependiendo del sistema puede haber ms disponibles).
2. Pila Projection (GL_PROJECTION). Esta pila contiene las matrices de proyeccin. Tiene un tamao mnimo de 2 elementos.
3. Pila Color (GL_PROJECTION). Utilizada para modicar los colores.
4. Pila Texture (GL_TEXTURE). Estas matrices se emplean para transformar las
coordenadas de textura.
Es posible cambiar la pila sobre la que especicaremos las transformaciones empleando la llamada a glMatrixMode(). Por ejemplo, para utilizar la pila de Modelview utilizaramos glMatrixMode(GL_MODELVIEW).
[365]
Y'
Posicin
final
X
Posicin
inicial
Z'
Sis
Re tema d
Unifverenciae
ersal
Y'
X'
Z'
Y'
Y'
Z'
X'
X'
Z'
Z
X
Z
X
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glRotatef(45,0,0,1);
glTraslatef(5,0,0);
dibujar_objeto();
C12
[366]
Idea del Sistema de Referencia Local. Tambin podemos imaginar que cada
objeto tiene un sistema de referencia local interno al objeto que va cambiando.
La composicin se realiza en el mismo orden que aparece en el cdigo fuente,
y siempre respecto de ese sistema de referencia local. De esta forma, como
se
muestra en la secuencia de la Figura 12.8 b), el objeto primero rota (lnea 3 ) por
lo que su sistema de referencia local queda rotado respecto del SRU, y respecto
de ese sistema de referencia local, posteriormente lo trasladamos 5 unidades
respecto del eje X (local).
Como hemos visto, es posible adems cargar matrices de transformacin denidas
por nuestros propios mtodos (podra ser interesante si empleramos, por ejemplo,
algn mtodo para calcular la proyeccin de sombras). Esto se realiza con la llamada
a funcin glLoadMatrix(). Cuando se llama a esta funcin se reemplaza la cima de
la pila de matrices activa con el contenido de la matriz que se pasa como argumento.
Si nos interesa es multiplicar una matriz denida en nuestros mtodos por el contenido de la cima de la pila de matrices, podemos utilizar la funcin glMultMatrix
que postmultiplica la matriz que pasamos como argumento por la matriz de la cima de
la pila.
12.3.5.
Veamos a continuacin un ejemplo sencillo que utiliza transformaciones jerrquicas. Deniremos un sistema planetario que inicialmente estar formado por el Sol y la
Tierra.
Listado 12.3: Sistema Planetario
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Ejemplo Planetario
Ejemplo sencillo para aanzar el orden de las transformaciones.
[367]
18
19
20
21
22
23 }
Como puede verse en el listado anterior, se ha incluido una variable global hours
que se incrementa cada vez que se llama a la funcin display. En este ejemplo, esta
funcin se llama cada vez que se pulsa cualquier tecla. Esa variable modela el paso
de las horas, de forma que la traslacin y rotacin
de
la Tierra se calcular a partir del
nmero de horas que han pasado (lneas 11 y 12 ). En la simulacin se ha acelerado
10 veces el movimiento de traslacin para que se vea ms claramente.
En ese punto dibujamos la primera esfera correspondiente al Sol. Hecho esto, ro-
tamos los grados correspondientes al movimiento de traslacin de la tierra
(lnea 16 )
y nos desplazamos 3 unidades respecto del eje X local del objeto (lnea 17 ). Antes de
dibujar
de rotacin de la tierra (lnea
la Tierra tendremos que realizar el movimiento
18 ). Finalmente dibujamos la esfera en la lnea 20 .
Veamos ahora un segundo ejemplo que utiliza glPushMatrix() y glPopMatrix()
para dibujar un brazo robtico sencillo. El ejemplo simplemente emplea dos cubos
(convenientemente escalados) para dibujar la estructura jerrquica de la gura 12.10.
En este ejemplo, se asocian manejadores de callback
interesante
en las lneas 19-42 .
Listado 12.4: Brazo Robtico
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
C12
[368]
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 }
desplazarnos
del trayecto (lnea 25 ), dibujar y recorrer la otra mitad 32 antes de dibujar la segunda
parte del robot.
12.4.
Ejercicios Propuestos
Captulo
13
13.1.
Inicio Casi-Manual
En este captulo no describiremos el
inicio totalmente manual de Ogre;
emplearemos las llamadas de alto
nivel para cargar plugins, inicializar
ventana, etc...
Inicializacin Manual
#include <Ogre.h>
#include "MyFrameListener.h"
class MyApp {
private:
369
[370]
6
Ogre::SceneManager* _sceneManager;
7
Ogre::Root* _root;
8
MyFrameListener* _framelistener;
9
10 public:
11
MyApp();
12
~MyApp();
13
int start();
14
void loadResources();
15
void createScene();
16 };
El programa principal nicamente tendr que crear una instancia de la clase MyApp
y ejecutar su mtodo start. La denicin de este mtodo puede verse en el siguiente
listado, en las lneas 13-44 .
13.1.1.
Inicializacin
En la lnea 14 se crea el objeto Root. Esta es la primera operacin que se debe
realizar antes de ejecutar cualquier otra operacin con Ogre. El constructor de Root
est sobrecargado, y permite que se le pase como parmetros el nombre del archivo
de conguracin de plugins (por defecto, buscar plugins.cfg en el mismo directorio
donde se encuentra el ejecutable), el de conguracin de vdeo (por defecto ogre.cfg),
y de log (por
ogre.log). Si no le indicamos ningn parmetro (como en el caso
defecto
de la lnea 14 ), tratar de cargar los cheros con el nombre por defecto.
El comportamiento del constructor de Root es diferente entre no especicar
parmetro, o pasar la cadena vaca . En el primer caso se buscarn los
archivos con el nombre por defecto. En el segundo caso, indicamos a Ogre
que cargaremos manualmente los plugins y la conguracin de vdeo.
En la lnea 16 se intenta cargar la conguracin de vdeo existente en el archivo ogre.cfg. Si el archivo no existe, o no es correcto, podemos
el dilogo que
abrir
empleamos en los ejemplos de las sesiones anteriores
(lnea 17 ), y guardar a continuacion los valores elegidos por el usuario (lnea 18 ). Con
el objeto Root creado,
podemos pasar a crear la ventana. Para ello, en la lnea 21 solicitamos al objeto Root
que nalice su inicializacin creando una ventana que utilice la conguracin que
eligi el usuario, con el ttulo especicado como segundo parmetro (MyApp en este
caso).
El primer parmetro booleano del mtodo indica a Ogre que se encarge de crear
automticamente la ventana. La llamada a este mtodo nos devuelve un puntero a un
objeto RenderWindow, que guardaremos en la variable window.
Lo que ser representado en la ventana vendr denido por el volumen de visualizacin de la cmara virtual. Asociado a esta cmara, crearemos una supercie (algo
que conceptualmente puede verse como el lienzo, o el plano de imagen) sobre la que se
dibujarn los objetos 3D. Esta supercie se denomina viewport. El Gestor de Escena
admite diversos modos de gestin, empleando el patrn de Factora, que son cargados
como plugins (y especicados en el archivo plugins.cfg). El parmetro indicado en la
[371]
llamada de 22 especica el tipo de gestor de escena que utilizaremos ST_GENERIC1 .
Esta gestin de escena mnima no est optimizada para ningn tipo de aplicacin en
particular, y resulta especialmente adecuada para pequeas aplicaciones, mens de
introduccin, etc.
Listado 13.2: MyApp.cpp
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
52
53
54
55
56
57
58
#include "MyApp.h"
MyApp::MyApp() {
_sceneManager = NULL;
_framelistener = NULL;
}
MyApp::~MyApp() {
delete _root;
delete _framelistener;
}
int MyApp::start() {
_root = new Ogre::Root();
if(!_root->restoreConfig()) {
_root->showConfigDialog();
_root->saveConfig();
}
// Si no se puede restaurar
// Abrimos ventana de config
// Guardamos la configuracion
_root->startRendering();
return 0;
// Clase propia
// Lo anadimos!
void MyApp::loadResources() {
Ogre::ConfigFile cf;
cf.load("resources.cfg");
Ogre::ConfigFile::SectionIterator sI = cf.getSectionIterator();
Ogre::String sectionstr, typestr, datastr;
while (sI.hasMoreElements()) {
// Mientras tenga elementos...
sectionstr = sI.peekNextKey();
Ogre::ConfigFile::SettingsMultiMap *settings = sI.getNext();
Ogre::ConfigFile::SettingsMultiMap::iterator i;
for (i = settings->begin(); i != settings->end(); ++i) {
typestr = i->first;
datastr = i->second;
Ogre::ResourceGroupManager::getSingleton().
addResourceLocation(datastr, typestr, sectionstr);
Estudiaremos en
C13
[372]
59
60
61
62 }
63
64 void MyApp::createScene() {
65
Ogre::Entity* ent = _sceneManager->createEntity("Sinbad.mesh");
66
_sceneManager->getRootSceneNode()->attachObject(ent);
67 }
asociado a esa
cmara y establecemos el color de fondo como negro (lneas 30-31 ).
manual la estudiaremos
en la seccin 13.1.2. La creacin de la escena (lneas 64-67 ), se realiza aadiendo la
entidad directamente al nodo Root.
Las lneas 39-40 crean un objeto de tipo FrameListener. Una clase FrameListener
es cualquier clase que implemente el interfaz de FrameListener, permitiendo que Ogre
llame a ciertos mtodos al inicio y al nal del dibujado de cada frame empleando el
patrn Observer. Veremos en detalle cmo se puede implementar esta clase en la
seccin 13.1.3.
Viewports...
Una cmara puede tener cero o ms
Viewports. Un uso comn de los
viewports es la generacin de imgenes dentro de otros viewports.
Veremos un uso de esta caracterstica en prximos captulos del mdulo.
[373]
13.1.2.
Carga de Recursos
Como puede verse, la nica seccin del archivo se denomina General. Crearemos un iterador que permita cargar cada una de las secciones y, dentro de cada una,
obtendremos los elementos que la denen.
La lnea 50 crea el SectionIterator asociado al chero de conguracin.
Mientras
existan elementos que procesar por el iterador (bucle while
de
las
lneas
52-60 ), ob
tiene la clave del primer elemento de la coleccin (lnea 53 ) sin avanzar al siguiente
(en el caso del ejemplo, obtiene la seccin General). A continuacin, obtiene en settings un puntero al valor del elemento actual de la coleccin, avanzando al siguiente.
Este valor en realidad otro mapa. Emplearemos un segundo iterador (lneas 56-69 )
para recuperar los nombres y valores de cada entrada de la seccin. En el chero de
ejemplo, nicamente iteraremos una vez dentro del segundo iterador, para obtener en
typestr la entrada a FileSystem, y en datastr el valor del directorio media.
Finalmente la llamada al ResourceGroupManager (lnea 61 ) solicita que se inicialicen todos
los grupos de recursos que han sido aadidos en el iterador anterior, en
la lnea 58 .
13.1.3.
FrameListener
#include <OgreFrameListener.h>
class MyFrameListener : public Ogre::FrameListener {
public:
bool frameStarted(const Ogre::FrameEvent& evt);
bool frameEnded(const Ogre::FrameEvent& evt);
};
C13
El chero de denicin de recursos se especica en la lnea 48 . Este archivo est
formado de diferentes secciones, que contienen pares de clave y valor. En el sencillo
ejemplo que vamos a denir en este captulo, tendremos el contenido denido en la
Figura 13.1.
[374]
#include "MyFrameListener.h"
bool MyFrameListener::frameStarted(const Ogre::FrameEvent& evt) {
std::cout << "Frame started" << std::endl;
return false;
}
bool MyFrameListener::frameEnded(const Ogre::FrameEvent& evt)
std::cout << "Frame ended" << std::endl;
return false;
}
13.2.
Uso de OIS
#include <OgreFrameListener.h>
#include <OgreRenderWindow.h>
#include <OIS/OIS.h>
class MyFrameListener : public Ogre::FrameListener {
private:
OIS::InputManager* _inputManager;
OIS::Keyboard* _keyboard;
public:
MyFrameListener(Ogre::RenderWindow* win);
~MyFrameListener();
bool frameStarted(const Ogre::FrameEvent& evt);
};
Sobre OIS
Recordemos que OIS no forma parte de la distribucin de Ogre. Es
posible utilizar cualquier biblioteca
para la gestin de eventos.
[375]
En las lneas 8-9 convertimos el manejador a cadena, y aadimos el par de objetos
(empleando la plantilla pair de std) como parmetros de OIS. Como puede verse el
convenio de Ogre y de OIS es similar a la hora de nombrar el manejador de la ventana
(salvo por el hecho de que OIS requiere que se especique como cadena, y Ogre lo
devuelve como un entero).
Con este parmetro, creamos un InputManager de OIS en 11 , que nos permitir
crear diferentes
interfaces para trabajar con eventos de teclado, ratn, etc. As, en las
#include "MyFrameListener.h"
MyFrameListener::MyFrameListener(Ogre::RenderWindow* win) {
OIS::ParamList param;
unsigned int windowHandle; std::ostringstream wHandleStr;
win->getCustomAttribute("WINDOW", &windowHandle);
wHandleStr << windowHandle;
param.insert(std::make_pair("WINDOW", wHandleStr.str()));
_inputManager = OIS::InputManager::createInputSystem(param);
_keyboard = static_cast<OIS::Keyboard*>
(_inputManager->createInputObject(OIS::OISKeyboard, false));
MyFrameListener::~MyFrameListener() {
_inputManager->destroyInputObject(_keyboard);
OIS::InputManager::destroyInputSystem(_inputManager);
}
bool MyFrameListener::frameStarted(const Ogre::FrameEvent& evt) {
_keyboard->capture();
if(_keyboard->isKeyDown(OIS::KC_ESCAPE)) return false;
return true;
}
La implementacin del mtodo frameStarted es muy sencilla. En la lnea 22 se
obtiene si hubo alguna pulsacin de tecla. Si se presion la tecla Escape (lnea 23 ), se
devuelve false de modo que Ogre nalizar el bucle principal de dibujado y liberar
todos los recursos que se estn empleando.
C13
Tanto el InputManager como el ojbeto de teclado deben ser eliminados explcitamente en el destructor de nuestro FrameListener. Ogre se encarga de liberar los
recursos asociados a todos sus Gestores. Como Ogre es independiente de OIS, no tiene constancia de su uso y es responsabilidad del programador liberar los objetos de
entrada de eventos, as como el propio gestor InputManager.
[376]
13.2.1.
9
),
y
el
nodo
(que
cmara (declarada igualmente como variable miembro
privada
en
contendr las entidades que queremos mover en 10 ).
En estos primeros ejemplos nos centraremos en el aspecto funcional de los mismos. En sucesivos captulos estudiaremos otras aproximaciones de diseo para la gestin de eventos.
Listado 13.7: MyFrameListener.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <Ogre.h>
#include <OIS/OIS.h>
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 "MyFrameListener.h"
MyFrameListener::MyFrameListener(Ogre::RenderWindow* win,
Ogre::Camera* cam, Ogre::SceneNode *node) {
OIS::ParamList param;
size_t windowHandle; std::ostringstream wHandleStr;
_camera = cam;
_node = node;
win->getCustomAttribute("WINDOW", &windowHandle);
wHandleStr << windowHandle;
param.insert(std::make_pair("WINDOW", wHandleStr.str()));
_inputManager = OIS::InputManager::createInputSystem(param);
_keyboard = static_cast<OIS::Keyboard*>
(_inputManager->createInputObject(OIS::OISKeyboard, false));
_mouse = static_cast<OIS::Mouse*>
(_inputManager->createInputObject(OIS::OISMouse, false));
MyFrameListener::~MyFrameListener() {
_inputManager->destroyInputObject(_keyboard);
_inputManager->destroyInputObject(_mouse);
OIS::InputManager::destroyInputSystem(_inputManager);
}
bool MyFrameListener::frameStarted(const Ogre::FrameEvent& evt) {
Ogre::Vector3 vt(0,0,0);
Ogre::Real tSpeed = 20.0;
Ogre::Real r = 0;
Ogre::Real deltaT = evt.timeSinceLastFrame;
_keyboard->capture();
a)
Espacio
b)
Espacio
c)
Espacio
d)
Espacio
[377]
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 }
13.3.
C13
Tiempo (Frames)
Tiempo (Frames)
[378]
La creacin
manual del plano se realiza en las lneas 8-11 del siguiente listado.
En la lnea 8 se especica que el plano tendr como vector normal, el vector unitario
en Y , y que estar situado a -5 unidades respecto del vector normal. Esta denicin
se corresponde con un plano innito (descripcin matemtica abstracta). Si queremos
representar el plano, tendremos que indicar a Ogre el tamao (nito) del mismo, as
como la resolucin que queremos aplicarle (nmero
horizontales y ver de divisiones
1 void MyApp::createScene() {
2
Ogre::Entity* ent1 = _sceneManager->createEntity("Sinbad.mesh");
3
Ogre::SceneNode* node1 =
4
_sceneManager->createSceneNode("SinbadNode");
5
node1->attachObject(ent1);
6
_sceneManager->getRootSceneNode()->addChild(node1);
7
8
Ogre::Plane pl1(Ogre::Vector3::UNIT_Y, -5);
9
Ogre::MeshManager::getSingleton().createPlane("pl1",
10
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
11
pl1,200,200,1,1,true,1,20,20,Ogre::Vector3::UNIT_Z);
12
13
Ogre::SceneNode* node2 = _sceneManager->createSceneNode("node2");
14
Ogre::Entity* grEnt = _sceneManager->createEntity("pEnt", "pl1");
15
grEnt->setMaterialName("Ground");
16
node2->attachObject(grEnt);
17
18
_sceneManager->setShadowTechnique(Ogre::
19
20
21
22
23
24
25 }
SHADOWTYPE_STENCIL_ADDITIVE);
Ogre::Light* light = _sceneManager->createLight("Light1");
light->setType(Ogre::Light::LT_DIRECTIONAL);
light->setDirection(Ogre::Vector3(1,-1,0));
node2->attachObject(light);
_sceneManager->getRootSceneNode()->addChild(node2);
El uso del MeshManager nos permite la creacin de planos, skyboxes, supercies de Bezier, y un largo etctera. En el caso de este ejemplo, se utiliza la llamada
a createPlane que recibe los siguientes parmetros indicados en orden: el primer
parmetro es el nombre de la malla resultante, a continuacin el nombre del grupo de
mallas (emplearemos el grupo por defecto).
El tercer parmetro es el objeto denicin
del plano innito (creado en la lnea 8 ). Los siguientes dos parmetros indican el ancho y alto del plano en coordenadas del mundo (200x200 en este caso). Los siguientes
dos parmetros se corresponden con el nmero de segmentos empleados para denir
el plano (1x1 en este caso), como se indica en la Figura 13.4. Estos parmetros sirven para especicar la resolucin geomtrica del plano creado (por si posteriormente
queremos realizar operaciones de distorsin a nivel de vrtice, por ejemplo).
El siguiente parmetro booleano indica (si es cierto) que los vectores normales se
calcularn perpendiculares al plano. El siguiente parmetro (por defecto, 1) indica el
conjunto de coordenadas de textura que sern creadas. A continuacin se indica el nmero de repeticin (uTile y vTile) de la textura (ver Figura 13.6). El ltimo parmetro
indica la direccin del vector Up del plano.
A continuacin en la lnea 15 asignamos al plano el material Ground. Este
material est denido en un archivo en el directorio media que ha sido cargado empleando el cargador de recursos explicado en la seccin 13.1.2. Este material permite
la recepcin de sombras, tiene componente de brillo difuso y especular, y una textura
de imagen basada en el archivo ground.jpg. Estudiaremos en detalle la asignacin de
materiales en el prximo captulo del documento.
[379]
Finalmente las lneas 18-22 se encargan de denir una fuente de luz dinmica
direccional 2 y habilitar el clculo de sombras. Ogre soporta 3 tipos bsicos de fuentes
de luz y 11 modos de clculo de sombras dinmicas, que sern estudiados en detalle
en prximos captulos.
13.4.
Uso de Overlays
La gestin de los Overlays en Ogre se realiza mediante el gestor de recursos llamado OverlayManager, que se encarga de cargar los scripts de denicin de los Overlays.
En un overlay pueden existir objetos de dos tipos: los contenedores y los elementos.
Los elementos pueden ser de tres tipos bsicos: TextArea empleado para incluir texto,
Panel empleado como elemento que puede agrupar otros elementos con un fondo jo
y BorderPanel que es igual que Panel pero permite que la textura de fondo se repita y denir un borde independiente del fondo. La denicin del Overlay del ejemplo
de esta seccin se muestra en la Figura 13.8. En el caso de utilizar un TextArea, ser
necesario especicar la carga de una fuente de texto. Ogre soporta texturas basadas
en imagen o truetype. La denicin de las fuentes se realiza empleando un chero de
extensin .fontdef (ver Figura 13.7).
Figura 13.6: Valores de repeticin en la textura. La imagen superior se corresponde con valores de
uT ile = 1, vT ile = 1. La imagen inferior implica un valor de 2
en ambos parmetros (repitiendo la
textura 2x2 veces en el plano).
En el listado de la Figura 13.8 se muestra la denicin de los elementos y contendores del Overlay, as como los cheros de material (extensin .material) asociados
al mismo. En el siguiente captulo estudiaremos en detalle la denicin de materiales
en Ogre, aunque un simple vistazo al chero de materiales nos adelanta que estamos
creando dos materiales, uno llamado panelInfoM, y otro matUCLM. Ambos utilizan
texturas de imagen. El primero de ellos utiliza una tcnica de mezclado que tiene en
cuenta la componente de opacidad de la pasada.
El primer contenedor de tipo Panel (llamado PanelInfo), contiene cuatro TextArea
en su interior. El posicionamiento de estos elementos se realiza de forma relativa al
posicionamiento del contenedor. De este modo, se establece una relacin de jerarqua
implcita entre el contenedor y los objetos contenidos. El segundo elemento contenedor (llamado logoUCLM) no tiene ningn elemento asociado en su interior.
C13
[380]
Figura 13.8: Denicin de los Overlays empleando cheros externos. En este chero se describen dos
paneles contenedores; uno para la zona inferior izquierda que contiene cuatro reas de texto, y uno para la
zona superior derecha que representar el logotipo de la UCLM.
rior. Por ejemplo, en el caso del panel logoUCLM, de tamao (150x120), la alineacin
horizontal se realiza a la derecha. El valor de -180 en el campo left indica que queremos que quede un margen de 30 pxeles entre el lado derecho de la ventana y el
extremo del logo.
Los Overlays tienen asociada una profundidad, denida en el campo zorder (ver
Figura 13.8), en el rango de 0 a 650. Overlays con menor valor de este campo sern
dibujados encima del resto. Esto nos permite denir diferentes niveles de despliegue
de elementos 2D.
Para nalizar estudiaremos la modicacin en el cdigo de MyApp y MyFrameListener.
En el siguiente listado se muestra que el constructor del FrameListener (lnea
7 ) necesita conocer el OverlayManager, cuya referencia se obtiene en la lnea 14 .
En realidad no sera necesario pasar el puntero al constructor, pero evitamos de esta
forma que el FrameListener tenga que solicitar la referencia.
horz_align
vert_align
left
top
width
height
material
caption
rotation
Descripcin
Puede ser relative (valor entre 0.0 y 1.0), o pixels (valor en pxeles). Por defecto es relative. En
coordenadas relativas, la coordenada superior izquierda es la (0,0) y la inferior derecha la (1,1).
Puede ser left (por defecto), center o right.
Puede ser top (por defecto), center o bottom.
Posicin con respecto al extremo izquierdo. Por defecto 0. Si el valor es negativo, se interpreta
como espacio desde el extremo derecho (con alineacin right).
Posicin con respecto al extremo superior. Por defecto 0. Si el valor es negativo, se interpreta como
espacio desde el extremo inferior (con alineacin vertical bottom).
Ancho. Por defecto 1 (en relative).
Alto. Por defecto 1 (en relative).
Nombre del material asociado (por defecto Ninguno).
Etiqueta de texto asociada al elemento (por defecto Ninguna).
ngulo de rotacin del elemento (por defecto sin rotacin).
Cuadro 13.1: Atributos generales de Elementos y Contenedores de Overlays.
C13
Atributo
metrics_mode
[381]
1024x768
1280x800
800x600
Figura 13.9: Resultado de ejecucin del ejemplo de uso de Overlays, combinado con los resultados parciales de las secciones anteriores. La imagen ha sido generada con 3 conguraciones de resolucin diferentes
(incluso con diferente relacin de aspecto): 1280x800, 1024x768 y 800x600.
Para nalizar, el cdigo del FrameListener denido en las lneas 6-16 del siguiente listado parcial, modica el valor del texto asociado a los elementos de tipo TextArea.
Hacemos uso de la clase auxiliar Ogre::StringConverter que facilita la conversin a
String de ciertos tipos de datos (vectores, cuaternios...).
Listado 13.10: Modicacin en MyApp.cpp
1 int MyApp::start() {
2
...
[382]
3
loadResources();
4
createScene();
5
createOverlay();
// Metodo propio para crear el overlay
6
...
7
_framelistener = new MyFrameListener(window, cam, node,
8
_overlayManager);
9
_root->addFrameListener(_framelistener);
10
...
11 }
12
13 void MyApp::createOverlay() {
14
_overlayManager = Ogre::OverlayManager::getSingletonPtr();
15
Ogre::Overlay *overlay = _overlayManager->getByName("Info");
16
overlay->show();
17 }
getPosition()));
oe = _overlayManager->getOverlayElement("camRotInfo");
oe->setCaption(Ogre::StringConverter::toString(_camera->
getDirection()));
oe = _overlayManager->getOverlayElement("modRotInfo");
Ogre::Quaternion q = _node->getOrientation();
oe->setCaption(Ogre::String("RotZ: ") +
Ogre::StringConverter::toString(q.getYaw()));
13
14
15
16
17
18
return true;
19 }
Captulo
14
Interaccin y Widgets
Csar Mora Castro
14.1.
383
[384]
Cada vez se realizan estudios ms serios sobre cmo desarrollar interfaces que
tengan ow, y sean capaces de brindar una mejor experiencia al usuario. La principal
diferencia con las interfaces de usuario de aplicaciones no orientadas al ocio, es que
estas centran su diseo en la usabilidad y la eciencia, no en el impacto sensorial.
Si un videojuego no consigue atraer y provocar sensaciones positivas al usuario, este
posiblemente no triunfar, por muy eciente que sea, o por muy original o interesante
que sea su trama.
A continuacin se detalla una lista de aspectos a tener en cuenta que son recomendables para aumentar el ow de un videojuego, aunque tradicionalmente se han
considerado perjudiciales a la hora de disear interfaces tradicionales segn las reglas
de interaccin persona-computador:
Mostrar la menor cantidad de informacin posible: durante el transcurso del
juego, es mejor no sobrecargar la interfaz con una gran cantidad de informacin.
En la gran mayora de videojuegos, toda esta conguracin se establece antes
de comenzar el juego (por ejemplo, en el Men), por lo que la interfaz queda
menos sobrecargada y distrae menos al usuario. Incluso el usuario puede tener
la opcin de mostrar menos informacin an si lo desea.
Inconsistencia de acciones: en algunos casos, es posible que se den inconsistencias en las acciones dependiendo del contexto del personaje. Por ejemplo, el
botn de saltar cuando el personaje est en tierra puede ser el de nadar si de
repente salta al agua.
Es importante mantener un nmero reducido de teclas (tanto si es por limitaciones de la plataforma, como una videoconsola, como para hacer la usabilidad
ms sencilla al usuario). Por lo tanto, hay que conseguir agrupar las acciones
en los botones segn su naturaleza. Por ejemplo, un botn lleva a cabo acciones
con objetos y peronajes (hablar, abrir una puerta), y otro movimientos de desplazamiento (saltar, escalar). Esto aumentar la intuitividad y la usabilidad del
videojuego, y por lo tanto, su ow.
Dicultar los objetivos al usuario: una de las reglas de oro de la interaccin
persona-computador es prevenir al usuario de cometer errores. Sin embargo, en
los videojuegos esta regla puede volverse contradictoria, pues en la mayora de
los casos el usuario busca en los videojuegos un sentimiento de satisfaccin que
se logra por medio de la superacin de obstculos y desafos.
Por lo tanto, es tambin de vital importancia conseguir un equilibrio en la dicultad del juego, que no sea tan difcil como para frustrar al usuario, pero no tan
fcil como para que resulte aburrido. En este aspecto la interfaz juega un papel
de mucho peso.
Se ha visto cmo el caso particular de las interfaces de los videojuegos puede contradecir reglas que se aplican en el diseo de interfaces de usuario clsicas en el campo
de la interaccin persona-computador. Sin embargo, existen muchas recomendaciones
que son aplicables a ambos tipos de interfaces. A continuacin se explican algunas:
Mantener una organizacin intuitiva: es importante que el diseo de los mens sean intuitivos. En muchos casos, esta falta de organizacin crea confusin
innecesaria al usuario.
Ofrecer una legibilidad adecuada: en algunos casos, darle demasiada importancia a la esttica puede implicar que la legibilidad del texto de las opciones o
botones se vea drsticamente reducida. Es importante mantener la funcionalidad
bsica.
[385]
C14
[386]
Figura 14.2: Extracto del men de conguracin de Nexuiz (izquierda), e interfaz de ScummVM (derecha).
14.1.1.
Men
Todos los videojuegos deben tener un Men desde el cual poder elegir los modos
de juego, congurar opciones, mostrar informacin adicional y otras caractersticas.
Dentro de estos mens, es muy frecuente el uso de Widgets como botones, barras deslizantes (por ejemplo, para congurar la resolucin de pantalla), listas desplegables
(para elegir idioma), o check buttons (para activar o desactivar opciones). Por eso es
importante disponer de un buen repertorio de Widgets, y que sea altamente personalizable para poder adaptarlos al estilo visual del videojuego.
En la Figura 14.2 se puede apreciar ejemplos de interfaces de dos conocidos juegos open-source. La interfaz de la izquierda, correspondiente al juego Nexuiz, muestra
un trozo de su dialogo de conguracin, mientras que la de la derecha corresponde a
la interfaz de ScummVM. En estos pequeos ejemplos se muestra un nmero considerable de Widgets, cuyo uso es muy comn:
Pestaas: para dividir las opciones por categoras.
Barras de desplazamiento: para congurar opciones que pueden tomar valores
muy numerosos.
Radio buttons: parmetros que pueden tomar valores excluyentes entre ellos.
Check buttons: activan o desactivan una opcin.
14.1.2.
HUD
[387]
CEGUIs mission
Como dice en su pgina web, CEGUI est dirigido a desarrolladores
de videojuegos que deben invertir
su tiempo en desarrollar buenos juegos, no creando subsistemas de interfaces de usuario.
14.2.
Introduccin CEGUI
CEGUI (Crazy Eddies GUI)[2] (Crazy Eddies GUI) es una biblioteca open source multiplataforma que proporciona entorno de ventanas y Widgets para motores grcos, en los cuales no se da soporte nativo, o es muy deciente. Es orientada a objetos
y est escrita en C++.
CEGUI es una biblioteca muy potente en pleno desarrollo, por lo que est
sujeta a continuos cambios. Todas las caractersticas y ejemplos descritos a lo
largo de este captulo se han creado utilizando la versin actualmente estable,
la 0.7.x. No se asegura el correcto funcionamiento en versiones anteriores o
posteriores.
CEGUI y Ogre3D
A lo largo de estos captulos,
se utilizar la frmula CEGUI/Ogre3D/OIS para proveer interfaz
grca, motor de rendering y gestin de eventos a los videojuegos,
aunque tambin es posible utilizarlo
con otras bibliotecas (ver documentacin de CEGUI).
CEGUI es muy potente y exible. Es compatible con los motores grcos OpenGL,
Direct3D, Irrlicht y Ogre3D.
De la misma forma que Ogre3D es nicamente un motor de rendering, CEGUI
es slo un motor de de gestin de Widgets, por lo que el renderizado y la gestin de
eventos de entrada deben ser realizadas por bibliotecas externas.
En sucesivas secciones se explicar cmo integrar CEGUI con las aplicaciones de
este curso que hacen uso de Ogre3D y OIS. Adems se mostrarn ejemplos prcticos
de las caractersticas ms importantes que ofrece.
C14
Una vez que se ha dado unas guas de estilo y la estructura bsicas para cualquier
videojuego, y se ha mostrado la importancia de los Widgets en ejemplos reales, se va
a estudiar el uso de una potente biblioteca que proporciona estos mismos elementos
para distintos motores grcos, para que el desarrollo de Widgets para videojuegos sea
lo menos problemtico posible.
[388]
14.2.1.
Instalacin
Estos pasos instalarn CEGUI bajo el directorio /usr/local/, por lo que es importante indicar en el Makele que las cabeceras las busque en el directorio
/usr/local/include/CEGUI.
Adems, algunos sistemas operativos no buscan las bibliotecas de enlazado
dinmico en /usr/local/lib por defecto. Esto produce un error al ejecutar la
aplicacin indicando que no puede encontrar las bibliotecas libCEGUI*. Para solucionarlo, se puede editar como superusuario el chero /etc/ld.so.conf,
y aadir la lnea include /usr/local/lib. Para que los cambios surtan efecto,
ejecutar sudo ldconfig.
14.2.2.
Inicializacin
[389]
Schemes: tienen la extensin .scheme. Denen el repertorio (o esquema) de Widgets que se utilizarn. Tambin indica qu scripts utilizar de otros tipos, como
por ejemplo el ImageSet, las Fonts o el LookNFeel.
Imageset: tienen la extensin .imageset. Dene cules sern la imgenes de los
elementos de la interfaz (punteros, barras, botones, etc).
LookNFeel: tienen la extensin .looknfeel. Dene el comportamiento visual de
cada uno de los Widgets para distintas acciones, por ejemplo, cmo se muestran
cuando se pasa el puntero del ratn o cuando se presionan,
Fonts: tienen la extensin .font. Cada uno de los scripts dene un tipo de fuente
junto con propiedades especcas como su tamao.
Layouts: tienen la extensin .layout. Cada script dene clases de ventanas concretas, con cada uno de sus elementos. Por ejemplo, una ventana de chat o una
consola.
Segn se avance en el captulo se ir estudiando ms en profundidad cul es el
funcionamiento y la estructura de estos recursos.
bootstrapSystem();
5
6
7
8
9
10
CEGUI::Scheme::setDefaultResourceGroup("Schemes");
CEGUI::Imageset::setDefaultResourceGroup("Imagesets");
CEGUI::Font::setDefaultResourceGroup("Fonts");
CEGUI::WindowManager::setDefaultResourceGroup("Layouts");
CEGUI::WidgetLookManager::setDefaultResourceGroup("LookNFeel");
Este mtodo de inicializar el renderer utilizando el bootstrapSystem fue introducido a partir de la versin 0.7.1. Para inicializar CEGUI en versiones
anteriores, es necesario referirse a su documentacion.
C14
En el siguiente cdigo se muestran los primeros pasos para poder integrar CEGUI
con Ogre3D, y de qu forma se inicializa.
[390]
[General]
FileSystem=media
[Schemes]
FileSystem=media/schemes
[Imagesets]
FileSystem=media/imagesets
[Fonts]
FileSystem=media/fonts
[Layouts]
FileSystem=media/layouts
[LookNFeel]
FileSystem=media/looknfeel
Y las opciones que hay que aadir al Makele para compilar son:
Listado 14.3: Flags de compilacin y de enlazado de CEGUI.
1
2
3
4
5
6
#Flags de compilado
CXXFLAGS += pkg-config --cflags CEGUI-OGRE
#Flags de enlazado
LDFLAGS += pkg-config --libs-only-L CEGUI-OGRE
LDLIBS += pkg-config --libs-only-l CEGUI-OGRE
Es importante incluir los ags de compilado, en la lnea 2, para que encuentre las
cabeceras segn se han indicado en el ejemplo de inicializacin.
Esto es slo el cdigo que inicializa la biblioteca en la aplicacin para que pueda
comenzar a utilizar CEGUI como interfaz grca, todava no tiene ninguna funcionalidad. Pero antes de empezar a aadir Widgets, es necesario conocer otros conceptos
primordiales.
14.2.3.
CEGUI::UDim(scale, offset)
Indica la posicin en una dimensin. El primer parmetro indica la posicin relativa, que toma un valor entre 0 y 1, mientras que el segundo indica un desplazamiento
absoluto en pxeles.
Por ejemplo, supongamos que posicionamos un Widget con UDim 0.5,20 en la dimensin x. Si el ancho de la pantalla fuese 640, la posicin sera 0.5*640+20 = 340,
mientras que si el ancho fuese 800, la posicin seri 0.5*800+20 = 420. En la Figura 14.5 se aprecian los dos ejemplos de forma grca. De esta forma, si la resolucin
de la pantalla cambia, por ejemplo, el Widget se reposicionar y se redimensionar de
forma automtica.
Teniendo en cuenta cmo expresar el posicionamiento en una nica dimensin
utilizando UDim, se denen dos elementoss ms.
(left, top)
Widget
(right, bottom)
Figura 14.4: rea rectangular denida por URect.
0.5
[391]
0.5
20 px
640x480
20 px
800x600
Coordenada x
Figura 14.5: Ejemplo del funcionamiento de UDim.
que est compuesto por dos UDim, uno para la coordenada x, y otro para la y, o
para el ancho y el alto, dependiendo de su uso.
El segundo elemento, se utiliza para denir un rea rectangular:
CEGUI::URect(UDim left, UDim top, UDim right, UDIM bottom)
Como muestra la Figura 14.4, los dos primeros denen la esquina superior izquierda, y los dos ltimos la esquina inferior derecha.
14.2.4.
Puesto que CEGUI es nicamente un motor de gestin de Widgets, tampoco incorpora la deteccin de eventos de entrada, por lo que es necesario inyectrselos desde
otra biblioteca. En este caso, se aprovechar la que ya se ha estudiado: OIS.
Considerando que se utiliza OIS mediante callbacks (en modo buffered), hay que
aadir las siguientes lneas para enviar a CEGUI la pulsacin y liberacin de teclas y
de los botones del ratn.
Listado 14.4: Inyeccin de eventos de pulsacin y liberacin de teclas a CEGUI.
1
2
3
4
5
6
7
8
9
10
11
12
13
return true;
C14
CEGUI::UVector2(UDim x, UDim y)
[392]
14 }
15
16 bool MyFrameListener::mousePressed(const OIS::MouseEvent& evt, OIS
::MouseButtonID id)
17 {
18
CEGUI::System::getSingleton().injectMouseButtonDown(
convertMouseButton(id));
19
return true;
20 }
21
22 bool MyFrameListener::mouseReleased(const OIS::MouseEvent& evt, OIS
::MouseButtonID id)
23 {
24
CEGUI::System::getSingleton().injectMouseButtonUp(
convertMouseButton(id));
25
return true;
26 }
Adems, es necesario convertir la forma en que identica OIS los botones del
ratn, a la que utiliza CEGUI, puesto que no es la misma, al contrario que sucede con
las teclas del teclado. Para ello se ha escrito la funcin convertMouseButton():
Listado 14.5: Funcin de conversin entre identicador de botones de ratn de OIS y CEGUI.
1 CEGUI::MouseButton MyFrameListener::convertMouseButton(OIS::
MouseButtonID id)
2 {
3
CEGUI::MouseButton ceguiId;
4
switch(id)
5
{
6
case OIS::MB_Left:
7
ceguiId = CEGUI::LeftButton;
8
break;
9
case OIS::MB_Right:
10
ceguiId = CEGUI::RightButton;
11
break;
12
case OIS::MB_Middle:
13
ceguiId = CEGUI::MiddleButton;
14
break;
15
default:
16
ceguiId = CEGUI::LeftButton;
17
}
18
return ceguiId;
19 }
Por otro lado, tambin es necesario decir a CEGUI cunto tiempo ha pasado desde
la deteccin del ltimo evento, por lo que hay que aadir la siguiente lnea a la funcin
frameStarted():
Listado 14.6: Orden que indica a CEGUI el tiempo transcurrido entre eventos.
1 CEGUI::System::getSingleton().injectTimePulse(evt.
timeSinceLastFrame)
[393]
14.3.
Primera aplicacin
En esta seccin se van a poner en prctica los primeros conceptos descritos para
crear una primera aplicacin. Esta aplicacin tendr toda la funcionalidad de Ogre
(mostrando a Sinbad), y sobre l un botn para salir de la aplicacin.
Es importante tener en cuenta que en CEGUI, todos los elementos son Windows. Cada uno de los Windows puede contener a su vez otros Windows. De
este modo, pueden darse situaciones raras como que un botn contenga a otro
botn, pero que en la prctica no suceden.
1 void MyApp::createGUI()
2 {
3
renderer = &CEGUI::OgreRenderer::bootstrapSystem();
4
CEGUI::Scheme::setDefaultResourceGroup("Schemes");
5
CEGUI::Imageset::setDefaultResourceGroup("Imagesets");
6
CEGUI::Font::setDefaultResourceGroup("Fonts");
7
CEGUI::WindowManager::setDefaultResourceGroup("Layouts");
8
CEGUI::WidgetLookManager::setDefaultResourceGroup("LookNFeel");
9
10
CEGUI::SchemeManager::getSingleton().create("TaharezLook.scheme")
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 }
;
CEGUI::System::getSingleton().setDefaultFont("DejaVuSans-10");
CEGUI::System::getSingleton().setDefaultMouseCursor("TaharezLook"
,"MouseArrow");
//Creating GUI Sheet
CEGUI::Window* sheet = CEGUI::WindowManager::getSingleton().
createWindow("DefaultWindow","Ex1/Sheet");
//Creating quit button
CEGUI::Window* quitButton = CEGUI::WindowManager::getSingleton().
createWindow("TaharezLook/Button","Ex1/QuitButton");
quitButton->setText("Quit");
quitButton->setSize(CEGUI::UVector2(CEGUI::UDim(0.15,0),CEGUI::
UDim(0.05,0)));
quitButton->setPosition(CEGUI::UVector2(CEGUI::UDim(0.5-0.15/2,0)
,CEGUI::UDim(0.2,0)));
quitButton->subscribeEvent(CEGUI::PushButton::EventClicked,
CEGUI::Event::Subscriber(&MyFrameListener::quit,
_framelistener));
sheet->addChildWindow(quitButton);
CEGUI::System::getSingleton().setGUISheet(sheet);
C14
[394]
Convenio de nombrado
CEGUI no exige que se siga ningn
convenio de nombrado para sus elementos. Sin embargo, es altamente recomendable utilizar un convenio jerrquico, utilizando la barra
/ como separador.
[395]
EventAlphaChanged
EventSized
Como se ha estudiado anteriormente, al igual que pasa con el grafo de escena de
Ogre3D, cada Window de CEGUI debe tener un padre. En la lnea 25 se asocia el botn al Sheet, y por ltimo, en la lnea 26 se indica a CEGUI cul es el Sheet que debe
mostrar.
A continuacin se muestra la denicin de la funcin que implementa el comportamiento. Como se puede apreciar, simplemente cambia el valor de una variable
booleana que controla la salida de la aplicacin. Es importante tener en cuenta que no
todas las funciones pueden ser utilizadas para implementar el comportamiento de los
elementos. En su signatura, deben de tener como valor de retorno bool, y aceptar un
nico parmetro del tipo const CEGUI::EventArgs& e
14.4.
Tipos de Widgets
Para comenzar a desarrollar una interfaz grca con CEGUI para un videojuego,
primero es necesario saber cul es exactamente el repertorio de Widgets disponible.
Como se ha estudiado en secciones anteriores, el repertorio como tal est denido
en el esquema escogido. Para los sucesivos ejemplos, vamos a utilizar el esquema
TaharezLook, utilizado tambin en la primera aplicacin de inicializacin. CEGUI
proporciona otros esquemas que ofrecen otros repertorios de Widgets, aunque los ms
comunes suelen estar implementados en todos ellos. Otros esquemas que proporciona
CEGUI son OgreTray, VanillaSkin y WindowsLook.
El script del esquema dene adems la apariencia y el comportamiento visual
de los Widgets. Para cambiar la apariencia visual, no es necesario crear un chero
esquema desde cero (lo que sera una ardua tarea). Basta con cambiar el script de los
ImageSet y de los Fonts. Esto se estudiar con ms profundidad en la Seccin 14.8.
A continuacin se muestra una pequea lista de los Widgets ms importantes denidos en el esquema TaharezLook:
Button
Check Box
Combo Box
Frame Window
C14
Listado 14.8: Funcin que implementa el comportamiento del botn al ser pulsado.
[396]
El siguiente paso en el aprendizaje de CEGUI es crear una interfaz que bien podra
servir para un juego, aprovechando las ventajas que proporcionan los scripts.
14.5.
Layouts
En la lnea 1 se escribe la cabecera del archivo xml, lo cual no tiene nada que ver
con CEGUI. En la lnea 2 se abre la etiqueta GUILayout, para indicar el tipo de script
y se cierra en la lnea 13.
A partir de aqu, se denen los Windows que contendr la interfaz (en CEGUI todo
es un Window!). Para declarar uno, se indica el tipo de Window y el nombre, como se
puede ver en la lnea 3. Dentro de l, se especican sus propiedades (lneas 4 y 5).
Estas propiedades pueden indicar el tamao, la posicin, el texto, la transparencia,
etc. Los tipos de Widgets y sus propiedades se denan en el esquema escogido, por
lo que es necesario consultar su documentacin especca. En la Seccin 14.6 se ver
un ejemplo concreto.
En la lnea 6 se muestra un comentario en xml.
Despus de la denicin de las propiedades de un Window, se pueden denir ms
Windows que pertenecern al primero, ya que los Widgets siguen una estructura jerrquica.
[397]
14.6.
Ejemplo de interfaz
El siguiente es el cdigo de un script layout que dene una ventana de conguracin, con distintos Widgets para personalizar el volumen, la resolucin, o el puerto
para utilizar en modo multiplayer. Adems incorpora un botn para aplicar los cambios y otro para salir de la aplicacin. En la Figura 14.7 se muestra el resultado nal.
Listado 14.10: Ejemplo de layout para crear una ventana de conguracin.
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
{{0.133,0},{0.027,0},{0.320,300},{0.127,300}}" />
<!-- Sonud parameter -->
<Window Type="TaharezLook/StaticText" Name="Cfg/SndText" >
<Property Name="Text" Value="Sonud Volume" />
<Property Name="UnifiedAreaRect" Value="
{{0.385,0},{0.0316,0},{0.965,0},{0.174,0}}" />
</Window>
<Window Type="TaharezLook/Spinner" Name="Cfg/SndVolume" >
<Property Name="Text" Value="Sonud Volume" />
<Property Name="StepSize" Value="1" />
<Property Name="CurrentValue" Value="75" />
<Property Name="MaximumValue" Value="100" />
<Property Name="MinimumValue" Value="0" />
<Property Name="UnifiedAreaRect" Value="
{{0.0598,0},{0.046,0},{0.355,0},{0.166,0}}" />
</Window>
<!-- Fullscreen parameter -->
<Window Type="TaharezLook/StaticText" Name="Cfg/FullScrText" >
<Property Name="Text" Value="Fullscreen" />
<Property Name="UnifiedAreaRect" Value="
{{0.385,0},{0.226,0},{0.965,0},{0.367,0}}" />
</Window>
<Window Type="TaharezLook/Checkbox" Name="Cfg/FullscrCheckbox"
>
<Property Name="UnifiedAreaRect" Value="
{{0.179,0},{0.244,0},{0.231,0},{0.370,0}}" />
</Window>
<!-- Port parameter -->
<Window Type="TaharezLook/StaticText" Name="Cfg/PortText" >
<Property Name="Text" Value="Port" />
<Property Name="UnifiedAreaRect" Value="
{{0.385,0},{0.420,0},{0.9656,0},{0.551,0}}" />
</Window>
<Window Type="TaharezLook/Editbox" Name="Cfg/PortEditbox" >
<Property Name="Text" Value="1234" />
<Property Name="MaxTextLength" Value="1073741823" />
<Property Name="UnifiedAreaRect" Value="
{{0.0541,0},{0.417,0},{0.341,0},{0.548,0}}" />
<Property Name="TextParsingEnabled" Value="False" />
</Window>
<!-- Resolution parameter -->
<Window Type="TaharezLook/StaticText" Name="Cfg/ResText" >
<Property Name="Text" Value="Resolution" />
<Property Name="UnifiedAreaRect" Value="
{{0.385,0},{0.60,0},{0.965,0},{0.750,0}}" />
</Window>
<Window Type="TaharezLook/ItemListbox" Name="Cfg/ResListbox" >
<Property Name="UnifiedAreaRect" Value="
{{0.0530,0},{0.613,0},{0.341,0},{0.7904,0}}" />
<Window Type="TaharezLook/ListboxItem" Name="Cfg/Res/Item1">
<Property Name="Text" Value="1024x768"/>
</Window>
C14
[398]
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
[399]
C14
[400]
14.7.
Este mtodo para disear interfaces de usuario directamente modicando los cheros xml .layout puede ser muy costoso. Existen un par de aplicaciones grcas para
el diseo de las interfaces. La primera CEGUI Layout Editor es muy avanzada, pero
se ha abandonado. Actualmente se est desarrollando un editor nuevo y actualizado,
CEGUI Unied Editor, pero todava no se considera estable.
Para compilar el editor, es necesario recompilar CEGUI con soporte para Python,
pues es necesaria la biblioteca PyCEGUI. Para compilarla en Ubuntu 11.04 y 11.10 se
puede seguir un tutorial disponible en la web de CEGUI[2], en el apartado HowTo, el
tutorial Build PyCEGUI from source for Linux.
Se puede encontrar un tutorial completo de cmo instalar el editor en:
www.cegui.org.uk/wiki/index.php/CEED
Al tratarse de una versin inestable, es mejor consultar este tutorial para mantener
los pasos actualizados de acuerdo a la ltima versin en desarrollo.
14.8.
Scripts en detalle
En esta Seccin se va a ver la estructura del resto de scripts que se utilizan para
construir la interfaz. Es importante conocerlos para poder cambiar la apariencia y
poder adaptarlas a las necesidades artsticas del proyecto.
14.8.1.
Scheme
Los esquemas contienen toda la informacin para ofrecer Widgets a una interfaz,
su apariencia, su comportamiento visual o su tipo de letra.
Listado 14.11: Estructura de un script scheme.
1 <?xml version="1.0" ?>
2 <GUIScheme Name="TaharezLook">
3
<Imageset Filename="TaharezLook.imageset" />
4
<Font Filename="DejaVuSans-10.font" />
5
<LookNFeel Filename="TaharezLook.looknfeel" />
6
<WindowRendererSet Filename="CEGUIFalagardWRBase" />
7
<FalagardMapping WindowType="TaharezLook/Button"
TargetType="CEGUI/PushButton" Renderer="Falagard/Button"
LookNFeel="TaharezLook/Button" />
8
<FalagardMapping WindowType="TaharezLook/Checkbox"
TargetType="CEGUI/Checkbox"
Renderer="Falagard/
ToggleButton" LookNFeel="TaharezLook/Checkbox" />
9 </GUIScheme>
Al igual que suceda con los layout, comienzan con una etiqueta que identican el
tipo de script. Esta etiqueta es GUIScheme, en la lnea 2.
De las lneas 3-6 se indican los scripts que utilizar el esquema de los otros tipos.
Qu conjunto de imgenes para los botones, cursores y otro tipo de Widgets mediante
el Imageset (lnea 3), qu fuentes utilizar mediante un Font (lnea 4), y el comportamiento visual de los Widgets a travs del LookNFeel (lnea 5). Adems se indica qu
sistema de renderizado de skin utilizar (lnea 6). CEGUI utiliza Falagard.
[401]
El resto se dedica a declarar el conjunto de Widgets. Para ello realiza un mapeado
entre el Widget que habr disponible en el esquema (por ejemplo, TaharezLook/Button, en la lnea 7), y los que ofrece CEGUI. Adems, por cada uno se indican varios
parmetros.
De este script usualmente se suele cambiar los Imageset y los Font para cambiar
la apariencia de la interfaz, y suministrar los desarrollados de forma propia.
14.8.2.
Font
14.8.3.
Imageset
Contiene las verdaderas imgenes que compondrn la interfaz. Este recurso es,
junto al de las fuentes, los que ms sujetos a cambio estn para poder adaptar la apariencia de la interfaz a la del videojuego.
Listado 14.13: Estructura de un script imageset.
1 <?xml version="1.0" ?>
2 <Imageset Name="TaharezLook" Imagefile="TaharezLook.tga"
De esta forma, se puede disear toda la interfaz con un programa de dibujo, unirlos
todos en un archivo, e indicar en el script imageset dnde se encuentra cada una. Para
ver a qu ms elementos se les puede aadir una imagen, consultar la referencia, o
estudiar los imageset proporcionados por CEGUI.
C14
[402]
14.8.4.
LookNFeel
Estos tipos de scripts son mucho ms complejos, y los que CEGUI proporciona
suelen ser ms que suciente para cualquier interfaz. An as, en la documentacin se
puede consultar su estructura y contenido.
14.9.
Una caracterstica muy interesante que se puede realizar con CEGUI es mostrar lo
que capta una cmara de Ogre en un Widget. Puede ser interesante para mostrar mapas
o la vista trasera de un coche de carreras, por ejemplo.
Como se aprecia en la Figura 14.10, la aplicacin mostrar a Sinbad de la forma
habitual, pero adems habr una ventana que muestra otra vista distinta, y sta a su
vez contendr un botn para salir de la aplicacin.
A continuacin se muestra el chero del layout. Despus de haber visto en la
Seccin 14.5 cmo funcionan los layouts, este no tiene ninguna complicacin. Se
trata de una nica FrameWindow (lnea 3) llamada CamWin, con el ttulo Back
Camera y una posicin y tamao determinados. Esta a su vez contiene dos Windows
ms: uno del tipo StaticImage (lnea 8), que servir para mostrar la imagen de la
textura generada a partir de una cmara secundaria de Ogre, para tener ese segundo
punto de vista; y uno del tipo Button (lnea 11), con el texto Exit.
El nico aspecto resaltable de este cdigo es que el Widget que muestra la imagen
(CamWin/RTTWindow) ocupa todo el area de su padre (su propiedad UniedAreaRect vale {{0,0},{0,0,},{1,0},{1,0}}. Esto quiere decir que ocupar toda la ventana
CamWin, que no toda la pantalla.
Listado 14.14: Layout del ejemplo.
1 <?xml version="1.0" encoding="UTF-8"?>
2 <GUILayout>
3
<Window Type="TaharezLook/FrameWindow" Name="CamWin" >
4
<Property Name="Text" Value="Back Camera" />
5
<Property Name="TitlebarFont" Value="DejaVuSans-10" />
6
<Property Name="TitlebarEnabled" Value="True" />
7
<Property Name="UnifiedAreaRect" Value="
8
9
10
11
12
13
14
15
16
{{0.6,0},{0.6,0},{0.99,0},{0.99,0}}" />
<Window Type="TaharezLook/StaticImage" Name="CamWin/RTTWindow"
>
<Property Name="UnifiedAreaRect" Value="
{{0,0},{0,0},{1,0},{1,0}}" />
</Window>
<Window Type="TaharezLook/Button" Name="ExitButton" >
<Property Name="Text" Value="Exit" />
<Property Name="UnifiedAreaRect" Value="
{{0.01,0},{0.01,0},{0.25,0},{0.15,0}}" />
</Window>
</Window>
</GUILayout>
Una vez tenemos la organizacin de la interfaz, es necesario dotarla de funcionalidad. A continuacin se muestran las modicaciones que hay que aadir para que se
pueda renderizar una cmara de Ogre en el Widget.
[403]
Listado 14.15: Inicializacin de la textura y del Widget que la mostrar.
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 CEGUI::Window* ex1 = CEGUI::WindowManager::getSingleton().
loadWindowLayout("render.layout");
34
35 CEGUI::Window* RTTWindow = CEGUI::WindowManager::getSingleton().
getWindow("CamWin/RTTWindow");
36
37 RTTWindow->setProperty("Image",CEGUI::PropertyHelper::imageToString
(&imageSet.getImage("RTTImage")));
38
39 //Exit button
40 CEGUI::Window* exitButton = CEGUI::WindowManager::getSingleton().
41
42
43
44
45
46
getWindow("ExitButton");
exitButton->subscribeEvent(CEGUI::PushButton::EventClicked,
CEGUI::Event::Subscriber(&MyFrameListener::quit,
_framelistener));
//Attaching layout
sheet->addChildWindow(ex1);
CEGUI::System::getSingleton().setGUISheet(sheet);
C14
1
2
3
4
5
6
7
8
9
10
[404]
El siguiente paso es crear el ViewPort. En la lnea 18 se obtiene el objeto RenderTexture a partir de la textura manual creada. En la lnea 20 se obtiene el objeto
ViewPort a partir del RenderTexture y utilizando la cmara que se quiere mostrar. En
este caso, _camBack. A este ViewPort se le indica que no dibuje los Overlays (lnea
21), aunque podra hacerlo sin problemas, y que se actualice en cada frame (lnea 22).
Adems se establece el color de fondo en negro (lnea 23).
Hasta ahora se ha creado la textura tex de Ogre que es capaz de actualizarse con
la imagen capturada por una cmara, tambin de Ogre, para tal efecto. El siguiente
paso es preparar CEGUI para mostrar imagen en uno de sus Widgets, y que adems
esa imagen la obtenga de la textura de Ogre.
En la lnea 25 se crea la textura guiTex de CEGUI. El objeto renderer especco
para Ogre proporciona la funcin createTexture(), que la crea a partir de una
de Ogre.
Como se vio en la Seccin 14.8.3, CEGUI est diseado para tratar las distintas
imgenes como porciones de un array de imgenes (ver Figura 14.9).
De este modo, primeramente hay que crear un Imageset a partir de la textura que
devuelve Ogre (ya en formato de CEGUI) en la lnea 27. A este conjunto de imgenes
se le ha llamado RTTImageset. Despus, hay que identicar qu porcin corresponde a la textura de la cmara de ese Imageset. En este caso, es la textura completa,
por lo que en la lnea 28 se dene la imagen con el nombre RTTImage. El primer
parmetro es el nombre, el segundo la esquina superior izquierda de la porcin que
dene la imagen (se indica el 0,0), el tercero el tamao de la porcin, que corresponde
al tamao de la textura, y el cuarto un offset.
Ya se ha conseguido obtener una imagen de CEGUI que se actualizar cada frame
con lo que capturar la cmara. Lo nico que falta es recuperar los Window denidos
en el layout e indicar que el Widget CamWin/RTTWindow muestre dicha textura.
En la lnea 33 se carga el layout como se hizo en anteriores ejemplos. Es importante hacerlo antes de comenzar a recuperar los Windows denidos en el layout, porque
de lo contrario no los encontrar.
Se recupera el Window que mostrar la imagen (del tipo StaticImage), llamado
CamWin/RTTWindow, en la lnea 35. En la siguiente lnea, la 37, se indica en la
propiedad Image de dicho Window que utilice la imagen RTTImage del conjunto
de imagenes.
Con esto ya es suciente para mostrar en el Widget la imagen de la cmara. Por
ltimo se aade la funcionalidad al botn de salida, en las lneas 40 y 41, como en
anteriores ejemplos, y se aade el layout al Sheet (lnea 45) y se establece dicho Sheet
por defecto (lnea 46).
14.10.
Formateo de texto
14.10.1.
Introduccin
[405]
[tag-name=value]
Color
Para cambiar el color del texto, se utiliza la etiqueta colour, usada de la siguiente
forma:
[colour=FFFF0000]
Esta etiqueta colorea el texto que la siguiese de color rojo. El formato en el que
se expresa el color es ARGB de 8 bits, es decir AARRGGBB. El primer parmetro
expresa la componente de transparencia alpha, y el resto la componente RGB.
14.10.3.
Formato
Para cambiar el formato de la fuente (tipo de letra, negrita o cursiva, por ejemplo)
es ms complejo ya que se necesitan los propios cheros que denan ese tipo de
fuente, y adems deben estar denidos en el script Font.
Estando seguro de tener los archivos .font y de tenerlos incluidos dentro del chero
del esquema, se puede cambiar el formato utilizando la etiqueta:
[font=Arial-Bold-10]
Esta en concreto corresponde al formato en negrita del tipo de letra Arial, de tamao 10. El nombre que se le indica a la etiqueta es el que se especic en el .scheme.
14.10.4.
Insertar imgenes
Insertar una imagen dentro de una cadena, al estilo de los emoticonos, es sencillo.
La estructura de la etiqueta es la siguiente:
[imageset=set:<imageset> image:<image>]
Una vez ms, CEGUI trata las imgenes individuales como parte de un Imageset,
por lo que hay que indicarle el conjunto, y la imagen.
C14
14.10.2.
[406]
Adems, existe otra etiqueta poder cambiar el tamao de las imgenes insertadas.
El formato de la etiqueta es el siguiente:
[image-size=w:<width_value> h:<height_value>]
El valor del ancho y del alto se da en pxeles, y debe ponerse antes de la etiqueta
que inserta la imagen. Para mostrar las imgenes en su tamao original, se deben poner
los valores de width y height a cero.
14.10.5.
Alineamiento vertical
Cuando un texto contiene distintos tamaos de letra, es posible congurar el alineamiento vertical de cada parte. El alto de una lnea concreta vendr denido por el
alto del texto con mayor tamao. El resto de texto, con menor tamao, podr alinearse
verticalmente dentro de ese espacio.
Los tipos de alineamiento vertical disponibles son:
top: lo alinea hacia arriba.
bottom: lo alinea hacia abajo.
center: lo centra verticalmente.
strecth: lo estira verticalmente para ocupar todo el alto.
El formato de la etiqueta es:
[vert-alignment=<tipo_de_alineamiento>]
14.10.6.
Padding
El padding consiste en reservar un espacio alrededor del texto que se desee. Para
denirlo, se indican los pxeles para el padding izquierdo, derecho, superior e inferior.
As, adems de el espacio que ocupe una determinada cadena, se reservar como un
margen el espacio indicado en el padding. Viendo la aplicacin de ejemplo se puede
apreciar mejor este concepto.
El formato de la etiqueta es:
[padding=l:<left_padding> t:<top_padding> r:<right_padding> b:<
bottom_padding>]
[407]
14.10.7.
{{0.05,0},{0.05,0},{0.95,0},{0.95,0}}"/>
<!-- Static Text -->
<Window Type="TaharezLook/StaticText" Name="FormatWin/Text1">
<Property Name="UnifiedAreaRect" Value="
{{0.05,0},{0.05,0},{0.95,0},{0.15,0}}"/>
</Window>
<!-- Other Static Text ... -->
<!-- Exit Button -->
<Window Type="TaharezLook/Button" Name="FormatWin/ExitButton">
<Property Name="Text" Value="Exit" />
<Property Name="UnifiedAreaRect" Value="
{{0,0},{0.95,0},{1,0},{1,0}}"/>
</Window>
</Window>
</GUILayout>
Y el siguiente listado muestra cada una de las cadenas que se han utilizado, junto
a las etiquetas:
Listado 14.17: Cdigo con los tags de formateo.
1 "Este color es [colour=FFFF0000] AZUL, mientras que [colour=
C14
[408]
2
3 "El tipo de letra puede [font=Batang-26]cambiar de un momento a
4
5 "Si pulsas aqui [image-size=w:40 h:55][image=set:TaharezLook
6
7 "[font=Batang-26] Soy GRANDE,
8
9 "En un lugar de la [padding=l:20 t:15 r:20 b:15]Mancha[padding=l
La primera cadena (lnea 1) utiliza las etiquetas del tipo colour para cambiar el
color del texto escrito a partir de ella. Se utilizan los colores rojo, verde y azul, en ese
orden.
La segunda (lnea 3) muestra cmo se pueden utilizar las etiquetas para cambiar
totalmente el tipo de fuente, siempre y cuando estn denidos los recursos .font y estos
estn reejados dentro el .scheme.
La tercera (lnea 5) muestra una imagen insertada en medio del texto, y adems
redimensionada. Para ello se utiliza la etiqueta de redimensionado para cambiar el tamao a 40x55, y despus inserta la imagen CloseButtonNormal, del conjunto TaharezLook
La cuarta (lnea 7) muestra una cadena con un texto (Soy GRANDE) de un tipo
de fuente con un tamao 30, y el resto con un tamao 10. Para el resto del texto, sobra
espacio vertical, por lo que se utiliza la etiqueta vertical-alignment para indicar dnde
posicionarlo.
Por ltimo, la quinta cadena (lnea 9), utiliza padding para la palabra Mancha. A
esta palabra se le reserva un margen izquierdo y derecho de 20 pxeles, y un superior
e inferior de 15.
14.11.
Caractersticas avanzadas
CEGUI es una biblioteca muy potente y exible que puede ser utilizada junto a
muchas otras para crear efectos visualmente muy impactantes. Algunas caractersticas
avanzadas que se han implementado son efectos de ventanas, como transparencia y
aspecto gelatinoso, o incluso incrustar un navegador dentro de una ventana.
Captulo
15
Materiales y Texturas
Carlos Gonzlez Morcillo
Iluminacin
Local
L
Punto
Vista
15.1.
Iluminacin
Global
L
Punto
Vista
Introduccin
Los materiales describen las propiedades fsicas de los objetos relativas a cmo
reejan la luz incidente. Obviamente, el aspecto nal obtenido ser dependiente tanto
de las propiedades del material, como de la propia denicin de las fuentes de luz. De
este modo, materiales e iluminacin estn ntimamente relacionados.
Desde los inicios del estudio de la ptica, investigadores del campo de la fsica
han desarrollado modelos matemticos para estudiar la interaccin de la luz en las
supercies. Con la aparicin del microprocesador, los ordenadores tuvieron suciente
potencia como para poder simular estas complejas interacciones.
As, usando un ordenador y partiendo de las propiedades geomtricas y de materiales especicadas numricamente es posible simular la reexin y propagacin de la
luz en una escena. A mayor precisin, mayor nivel de realismo en la imagen resultado.
409
[410]
a)
b)
Figura 15.2: Ejemplo de resultado utilizando un modelo de iluminacin local y un modelo de iluminacin
global con la misma conguracin de fuentes de luz y propiedades de materiales. a) En el modelo de iluminacin local, si no existen fuentes de luz en el interior de la habitacin, los objetos aparecern totalmente a
oscuras, ya que no se calculan los rebotes de luz indirectos. b) Los modelos de iluminacin global tienen
en cuenta esas contribuciones relativas a los rebotes de luz indirectos.
Esta conexin entre la simulacin del comportamiento de la luz y el nivel de realismo queda patente en las aproximaciones existentes de diferentes mtodos de render.
Una ecuacin que modela el comportamiento fsico de la luz, ampliamente aceptada
por la comunidad, es la propuesta por Kajiya en 1986. De forma general podemos
decir que a mayor simplicacin en la resolucin de los trminos de esta ecuacin
tendremos mtodos menos realistas (y computacionalmente menos costosos).
A un alto nivel de abstraccin, podemos realizar una primera taxonoma de mtodos de render entre aquellos que realizan una simulacin de iluminacin local, teniendo en cuenta nicamente una interaccin de la luz con las supercies, o los mtodos
de iluminacin global que tratan de calcular todas 1 las interacciones de la luz con las
supercies de la escena. La Figura 15.2 muestra el resultado de renderizar la misma
escena con un mtodo de iluminacin local y uno global. Los modelos de iluminacin
global incorporan la iluminacin directa que proviene de la primera interaccin de las
supercies con las fuentes de luz, as como la iluminacin indirecta reejada por otras
supercies existentes en la escena.
La potencia de clculo actual hace inviable el uso de mtodos de iluminacin global. Se emplean aproximaciones de preclculo de la iluminacin, que sern estudiadas
en el captulo de iluminacin. As, en los motores grcos actuales como Ogre, los
materiales denen cmo se reeja la luz en los objetos (pero no su contribucin con
otros objetos), empleando un esquema de iluminacin local.
15.2.
Modelos de Sombreado
la escena, las aproximaciones de iluminacin global se ocuparn de calcular algunas de estas interacciones,
tratando de minimizar el error de muestreo.
Sombreado
Ambiental
[411]
Sombreado
Difuso
Sombreado
Especular
Sombreado
de Emisin
Sombreado
Final
Sombreado difuso. Muchos objetos del mundo real tienen una acabado eminentemente mate (sin brillo). Por ejemplo, el papel o una tiza pueden ser supercies con sombreado principalmente difuso. Este tipo de materiales reejan
la luz en todas las direcciones, debido principalmente a las rugosidades microscpicas del material. Como efecto visual, el resultado de la iluminacin es
mayor cuando la luz incide perpendicularmente en la supercie. La intensidad
nal viene determinada por el ngulo que forma la luz y la supercie (es independiente del punto de vista del observador). En su expresin ms simple, el
modelo de reexin difuso de Lambert dice que el color de una supercie es
proporcional al coseno del ngulo formado entre la normal de la supercie y el
vector de direccin de la fuente de luz (ver Figura 15.3).
Sombreado especular. Es el empleado para simular los brillos de algunas supercies, como materiales pulidos, pintura plstica, etc. Una caracterstica principal del sombreado especular es que el brillo se mueve con el observador. Esto
implica que es necesario tener en cuenta el vector del observador. La dureza del
brillo (la cantidad de reejo del mismo) viene determinada por un parmetro h.
A mayor valor del parmetro h, ms concentrado ser el brillo. El comportamiento de este modelo de sombreado est representado en la Figura 15.5, donde
r es el vector reejado del l (forma el mismo ngulo con n) y e es el vector
que se dirige del punto de sombreado al observador. De esta forma, el color nal
de la supercie es proporcional al ngulo .
L
n
l
Figura 15.5: En el modelo de sombreado especular, el color c se obtiene como c (r e)h . Los vectores r y e deben estar normalizados.
Sombreado ambiental. Esta componente bsica permite aadir una aproximacin a la iluminacin global de la escena. Simplemente aade un color base
independiente de la posicin del observador y de la fuente de luz. De esta forma se evitan los tonos absolutamente negros debidos a la falta de iluminacin
global, aadiendo este trmino constante. En la Figura 15.4 se puede ver la
componente ambiental de un objeto sencillo.
Sombreado de emisin. Finalmente este trmino permite aadir una simulacin de la iluminacin propia del objeto. No obstante, debido a la falta de interaccin con otras supercies (incluso con las caras poligonales del propio objeto), suele emplearse como una alternativa al sombreado ambiental a nivel local.
El efecto es como tener un objeto que emite luz pero cuyos rayos no interactan
con ninguna supercie de la escena (ver Figura 15.4).
C15
[412]
15.3.
Mapeado de Texturas
[413]
v=0.71
u=0.35
Asociacin de
coordendas de textura
UV a cada vrtice de
cada cara del modelo.
Resultado final
del modelo
texturizado
C15
Figura 15.7: Asignacin de coordenadas UV a un modelo poligonal. Esta operacin suele realizarse con el
soporte de alguna herramienta de edicin 3D.
Textura a Mapear
Proyeccin
Plana
Proyeccin
Cbica
Proyeccin
Cilndrica
Proyeccin
Esfrica
Figura 15.8: Mtodos bsicos de mapeado ortogonal sobre una esfera. Los bordes marcados con lneas de
colores en la textura a mapear en la izquierda se proyectan de forma distinta empleado diversos mtodos de
proyeccin.
15.4.
Materiales en Ogre
[414]
Material
Technique 1
Technique 2
Pass 1
Pass 1
Pass 2
Pass 2
...
Pass i
Technique N
Pass 1
...
...
Pass j
Pass 2
...
Pass k
Figura 15.10: Descripcin de un material en base a tcnicas y pasadas. El nmero de tcnicas y pasadas
asociadas a cada tcnica puede ser diferente.
Con la idea de realizar estas optimizaciones, Ogre dene que por defecto los materiales son compartidos entre objetos. Esto implica que el mismo puntero que referencia
a un material es compartido por todos los objetos que utilizan ese material. De este
modo, si queremos cambiar la propiedad de un material de modo que nicamente afecte a un objeto es necesario clonar este material para que los cambios no se propaguen
al resto de objetos.
Los materiales de Ogre se denen empleando tcnicas y esquemas. Una Tcnica
puede denirse como cada uno de los modos alternativos en los que puede renderizarse un material. De este modo, es posible tener, por ejemplo, diferentes niveles de
detalle asociados a un material. Los esquemas agrupan tcnicas, permitiendo denir
nombres y grupos que identiquen esas tcnicas como alto nivel de detalle, medio
rendimiento, etc.
15.4.1.
Composicin
Un material en Ogre est formado por una o varias tcnicas, que contienen a su
vez una o varias Pasadas (ver Figura 15.10). En cada momento slo puede existir
una tcnica activa, de modo que el resto de tcnicas no se emplearn en esa etapa de
render.
Una vez que Ogre ha decidido qu tcnica emplear, generar tantas pasadas (en el
mismo orden de denicin) como indique el material. Cada pasada dene una operacin de despliegue en la GPU, por lo que si una tcnica tiene cuatro pasadas asociadas
a un material tendr que desplegar el objeto tres veces en cada frame.
Las Unidades de Textura (Texture Unit) contienen referencias a una nica textura.
Esta textura puede ser generada en cdigo (procedural), puede ser obtenida mediante
un archivo o mediante un ujo de vdeo. Cada pasada puede utilizar tantas unidades
de textura como sean necesarias. Ogre optimiza el envo de texturas a la GPU, de
modo que nicamente se descargar de la memoria de la tarjeta cuando no se vaya a
utilizar ms.
[415]
Atributo
lod_values
Descripcin
Lista de distancias para aplicar diferentes niveles de detalle. Est relacionado
con el campo lod_strategy (ver API de Ogre).
Admite valores on (por defecto) y off. Indica si el objeto slido puede recibir
sombras. Los objetos transparentes nunca pueden recibir sombras (aunque s
arrojarlas, ver el siguiente campo).
Indica si el material transparente puede arrojar sombras. Admite valores on y
off (por defecto).
Permite crear alias de un nombre de textura. Primero se indica el nombre del
alias y despus del nombre de la textura original.
receive_shadows
transparency_casts_shadows
set_texture_alias
Atributo
ambient
diffuse
specular
Formato
r g b [a]
r g b [a]
r g b [a] h
emissive
scene_blend
r g b [a]
(Ver valores)
depth_check
on | off
lighting
shading
on | off
(Ver valores)
polygon_mode
(Ver valores)
Descripcin
Valor de sombreado ambiente (por defecto 1.0 1.0 1.0 1.0).
Valor de sombreado difuso (por defecto 1.0 1.0 1.0 1.0).
Valor de sombreado especular (por defecto 0.0 0.0 0.0 0.0). El valor de dureza
(shininess) puede ser cualquier valor >0.
Valor de emisin (por defecto 0.0 0.0 0.0 0.0).
Tipo de mezclado de esta pasada con el resto de la escena. Por defecto no se
realiza mezclado. Si se especica, puede tomar valores entre add, modulate,
colour_blend y alpha_blend.
Por defecto on. Indica si se utilizar el depth-buffer para comprobar la profundidad.
Por defecto on. Indica si se emplear iluminacin dinmica en la pasada.
Indica el tipo de mtodo de interpolacin de iluminacin a nivel de vrtice. Se
especican los valores de interpolacin de sombreado at o gouraud o phong.
Por defecto se emplea el mtodo de gouraud.
Indica cmo se representarn los polgonos. Admite tres valores: solid (por
defecto), wireframe o points.
Cuadro 15.2: Atributos generales de las Pasadas. Ver API de Ogre para una descripcin completa de todos los atributos soportados.
15.4.2.
Ejemplo de Materiales
C15
[416]
Figura 15.13: Un material ms complejo que incluye la denicin de una texture_unit con diversas componentes de sombreado.
Los mapas de entorno se utilizan para simular la reexin del mundo (representado
en el mapa) dependiendo de la relacin entre las normales de las caras poligionales y la
posicin del observador. En este caso, se indica a Ogre que el tipo de mapa de entorno
es esfrico (tipo ojo de pez, ver Figura 15.12). El operador de colour_op empleado
indica cmo se combinar el color de la textura con el color base del objeto. Existen
diversas alternativas; mediante modulate se indica a Ogre que multiplique el color base
(en este caso, un color amarillo indicado en la componente difusa del color) y el color
del mapa.
A continuacin se describen algunas de las propiedades generales de los materiales. En la tabla 15.1 se resumen los atributos generales ms relevantes empleados
en la denicin de materiales, mientras que las tablas 15.2 y 15.3 resumen los atributos globales ms utilizados en la denicin de las pasadas y unidades de textura
resepectivamente. Cada pasada tiene asociada cero o ms unidades de textura. En los
siguientes ejemplos veremos cmo combinar varias pasadas y unidades de textura para
denir materiales complejos en Ogre.
[417]
Descripcin
Especica el nombre de la textura (esttica) para esta texture_unit. Permite especicar el tipo, y el
uso o no de canal alpha separado.
Permite utilizar un conjunto de imgenes como textura animada. Se especica el nmero de frames
y el tiempo (en segundos).
Filtro empleado para ampliar o reducir la textura. Admite valores entre none, bilinear (por defecto), trilinear o anisotropic.
Permite determinar cmo se mezcla la unidad de textura. Admite valores entre replace, add, modulate (por defecto) o alpha_blend.
Versin extendida del atributo anterior, que permite especicar con mucho mayor detalle el tipo
de mezclado.
Uso de mapa de entorno. Si se especica (por defecto est en off, puede tomar valores entre spherical, planar, cubic_reection y cubic_normal.
Permite ajustar la rotacin de la textura. Requiere como parmetro el ngulo de rotacin (en contra
de las agujas del reloj).
Ajusta la escala de la textura, especicando dos factores (en X e Y).
Cuadro 15.3: Atributos generales de las Unidades de Textura. Ver API de Ogre para una descripcin completa de todos los atributos soportados
Mapas UV
El mapeado UV es el estndar en
desarrollo de videojuegos, por lo
que prestaremos especial atencin
en este documento.
Mapeado UV en Blender
Isla UV
Se dene una isla en un mapa UV
como cada regin que contiene vrtices no conectados con el resto.
Como vimos en el captulo 11, para comenzar es necesario aplicar una operacin
de despliegue del modelo para obtener una o varias regiones en la ventana de UV/Image Editor . Esta operacin de despliegue se realiza siempre en modo edicin. Con
, y tras aplicar el despliegue
el objeto en modo edicin,
en los botones de edicin
C15
15.5.
[418]
Unwrap. Esta opcin despliega las caras del objeto tratando de obtener
una conguracin que rellene de la mejor forma posible la textura, empleando informacin topolgica de la malla (cmo estn conectadas las
caras de la malla). Si es posible, cada cara desplegada tendr su propia
regin de la imagen sin solapar con otras caras.
Figura 15.18: Ventana para la creacin de una nueva textura para el
mapeado UV.
[419]
Figura 15.19: Resultado obtenido tras aplicar diferentes mtodos de despliegue (unwrap) al modelo de
Suzanne con todas las caras seleccionadas: a) Unwrap, b) Cube, c) Cylinder, d) Sphere, e) Project from
view, f) Reset, g) Lightmap y h) Smart UV.
C15
[420]
Figura 15.23: Utilizacin de costuras y despliegue del modelo utilizando diferentes tcnicas de despliegue. En la imagen de la izquerda se marcan perfectamente los seams denidos para recortar el modelo
adecuadamente.
15.5.1.
Costuras
En el despliegue de mallas complejas, es necesario ayudar a los mtodos de despliegue deniendo costuras (seams). Estas costuras servirn para denir las zonas de
recorte, guiando as al mtodo de desplegado.
Para denir una costura basta con elegir las aristas que la denen en modo edicin,
tal y como se muestra en la Figura 15.23 . La seleccin de bucles de aristas (como en
el caso del bote de spray del modelo) puede realizarse cmodamente seleccionando
una de las aristas del bucle y empleando la combinacin de teclas Control E
Edge Loop. Cuando tenemos
la zona de recorte seleccionada, deniremos la costura
mediante la combinacin Control E Mark Seam. En este men aparece igualmente
la opcin para eliminar una costura Clear Seam.
[421]
puede
te desplazarse
empleando los operadores habituales de traslacin G , rotacin R
y escalado S . En esta ventana puede igualmente
emplearse el atajo de teclado para seleccionar los vrtices conectados (link) L y seleccionar rpidamente regiones
desplegadas
(incrementalmente si pulsamos L mientras mantenemos pulsada la tecla
Shift ).
15.6.
En la denicin del Material4 se utiliza una textura de tipo cebra donde las bandas
negras son totalmente transparentes. En la pasada de este material se utiliza el atributo
scene_blend que permite especicar el tipo de mezclado de esta pasada con el resto de
contenido de la escena. Como veremos en el ejemplo del Material6, la pricipal diferencia entre las operaciones de mezclado a nivel de pasada o a nivel de textura es que
las primeras denen la mezcla con el contenido global de la escena, mientras que las
segundas estn limitadas entre capas de textura. Mediante el modicador alpha_blend
se indica que utilizaremos el valor alpha de la textura. A continuacin estudiaremos
las opciones permitidas por el atributo scene_blend en sus dos versiones.
El formato de scene_blend permite elegir cuatro parmetros en su forma bsica:
add. El color del resultado de la pasada se suma al resultado de la escena.
modulate. El resultado de la pasada se multiplica con el resultado de color de
la escena.
Figura 15.25: Despliegue del modelo de los ejemplos. Se ha empleado una operacin de despliegue cinlndrico para la mandbula inferior,
Smart Projections para los dientes
y el Unwrap bsico para el resto del
objeto.
colour_blend. El color resultado de la pasada se mezcla empleando la componente de brillo de los colores.
alpha_blend. Este modo utiliza la informacin de transparencia del resultado.
El atributo scene_blend permite otro formato mucho ms general y exible, que
requiere dos parmetros: scene_blend src dest. En esta segunda versin, el color nal
se obtiene como: c = (texture * src) + (scene_pixel * dest). En esta segunda versin,
src y dest son factores de mezclado, entre 10 posibles valores:
one. Valor constante 1.0.
zero. Valor constante 0.0.
C15
En esta seccin estudiaremos algunos ejemplos de denicin de materiales empleando diversas unidades de tetura en Ogre. Nos centraremos en el uso de algunos
operadores para la composicin de diferentes unidades de textura. Las Figuras 15.24
y 15.27 muestran el resultado de denicin de estos materiales y texturas. Veamos
algunas de las propiedades empleadas en su denicin.
[422]
Figura 15.24: Ejemplos de denicin de algunos materiales empleando diversos modos de composicin en
Ogre.
C15
Consulta el manual!
[423]
[424]
15.7.
Render a Textura
El primerpaso para aplicar el Render a Textura es obtener un objeto de tipo TexturePtr (lneas 1-3 ), que permite crear una textura manualmente, indicando el nombre
de la textura (RttT), y las propiedades de tamao (512x512 pxeles), as como el
formato de color (32Bits en RGB, sin canal alfa PF_R8G8B8). El ltimo parmetro
de TU_RENDERTARGET indica a Ogre el tipo de uso que haremos de la textura.
Render a Textura
Existen multitud de aplicaciones en
videojuegos para utilizar Render a
Textura. Por ejemplo, los espejos de
cualquier simulador de conduccin
o cmaras de vigilancia dentro del
juego, as como multitud de efectos grcos (como espejos, motion
blur...) y de postproduccin que se
realizan empleando esta tcnica.
[425]
a)
c)
d)
Figura 15.28: Construccin del ejemplo de RTT. a) Modelo empleado para desplegar la textura de RTT.
El modelo se ha denido con tres materiales; uno para la carcasa, otro para las letras del logotipo (ambos
desplegados en c), y uno para la pantalla llamado pantallaTV, desplegado en b). En d) se muestra el
resultado de la ejecucin del programa de ejemplo.
Listado 15.1: Fragmento de CreateScene (MyApp.cpp).
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
C15
b)
[426]
33 }
En la lnea 5 se obtiene un puntero a un RenderTexture que es una especializacin
de la clase RenderTarget especca para renderizar sobre una textura.
los overlays en la lnea 16 ).
En la lnea 17 indicamos que la textura se actualice automticamente (gestionada
por el bucle principal de Ogre). En otro caso, ser necesario ejecutar manualmente el
mtodo update del RenderTarget.
el que emplearemos la
Las lneas 19-25 declaran manualmente el material sobre
19-20
Texture Unit con la textura anteriormente denida.
En
creamos un material
pasada dene sus propiedades en las lneas 23-25 , y aade como TextureUnit a la
textura RttT.
El ltimo bucle recorre todas las SubEntities que forman a la entidad de la televisin. En el caso
que el material asociado a la subentidad sea el llamado pan de
31
tallaTV
(lnea
), le asignamos el material que hemos creado anteriormente (lnea
32 ).
15.7.1.
Texture Listener
#include <Ogre.h>
using namespace std;
using namespace Ogre;
class MyTextureListener : public RenderTargetListener {
private:
Entity* _ent;
public:
MyTextureListener(Entity *ent);
~MyTextureListener();
virtual void preRenderTargetUpdate(const RenderTargetEvent& evt);
virtual void postRenderTargetUpdate(const RenderTargetEvent& evt);
};
[427]
A continuacin se muestra el listado que dene la clase MyTextureListener. La
funcionalidad ha sido comentada anteriormente.
Listado 15.3: MyTextureListener.cpp
1
2
3
4
5
6
7
8
9
#include "MyTextureListener.h"
MyTextureListener::MyTextureListener(Entity* ent){
_ent = ent;
}
MyTextureListener::~MyTextureListener(){ }
void MyTextureListener::preRenderTargetUpdate(const
RenderTargetEvent& evt) {
10
cout << "preRenderTargetupdate" << endl;
11
_ent->setVisible(false);
12 }
13
14 void MyTextureListener::postRenderTargetUpdate(const
RenderTargetEvent& evt) {
15.7.2.
Espejo (Mirror)
En las lneas 8-12 se dene la cmara que utilizaremos para crear el efecto de
mirror. Esta cmara debe tener la misma posicin y orientacin que la cmara desde
donde percibimos la escena, pero debe ser independiente (ya que activaremos la reexin sobre un plano, y modicaremos su plano de recorte cercano en las lneas 42-43).
Esta cmara para el efecto de espejo (llamada MirrorCamera en este ejemplo) debe
estar siempre correctamente alineada con la cmara principal. En este ejemplo, la cmara principal es esttica, por lo que tampoco modicaremos la posicin y orientacin
de la MirrorCamera. Queda como ejercicio propuesto para el lector aadir movimiento a la cmara principal, actualizando anlogamente en cada frame la posicin de la
cmara Mirror.
En las lneas 24-30 se denen las propiedades generales del material. La lnea 31
nos permite generar las coordenadas de textura segn la cmara que se le pasa como
argumento, de modo que da la impresin de que la textura ha sido proyectada sobre la
supercie. De esta forma, la textura que generamos ser posteriormente renderizada
utilizando el modelo de proyeccin denido por la cmara de Mirror.
C15
15
cout << "postRenderTargetupdate" << endl;
16
_ent->setVisible(true);
17 }
[428]
Para concluir, en las lneas 42-43 se conguran los aspectos relativos a la cmara
Mirror en relacin al plano de reexin. La llamada a enableReection hace que se
modique el Frustum de la cmara de modo que renderice empleando la reexin con
respecto del plano que se le pasa como argumento.
Finalmente, la llamada a enableCustomNearClipPlane permite recortar la geometra situada debajo del plano pasado como argumento, de modo que nicamente la
geometra que est situada sobre el plano ser nalmente desplegada en el reejo,
evitando as errores de visualizacin.
Listado 15.5: Denicin del Material tipo Espejo
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
Captulo
16
Iluminacin
Carlos Gonzlez Morcillo
16.1.
Introduccin
Una pequea parte de los rayos de luz son visibles al ojo humano. Aquellos rayos
que estn denidos por una onda con longitud de onda entre 700 y 400nm. Variando
las longitudes de onda obtenemos diversos colores.
En grcos por computador es habitual emplear los denominados colores-luz, donde el Rojo, Verde y Azul son los colores primarios y el resto se obtienen de su combinacin. En los colores-luz el color blanco se obtiene como la suma de los tres colores
bsicos.
Figura 16.1: Gracias a la proyeccin de sombras es posible conocer la posicin relativa entre objetos. En la imagen superior no es posible determinar si el modelo de Suzanne reposa sobre algn escaln o
est otando en el aire. La imagen
inferior, gracias al uso de sombras
elimina esa ambigedad visual.
429
[430]
1. La luz es emitida por las fuentes de luz de la escena (como el sol, una lmpara de
luz situada encima de la mesa, o un panel luminoso en el techo de la habitacin).
2. Los rayos de luz interactan con los objetos de la escena. Dependiendo de las
propiedades del material de estos objetos, parte de la luz ser absorbida y otra
parte reejada y propagada en diversas direcciones. Todos los rayos que no son
totalmente absorbidos continuarn rebotando en el entorno.
3. Finalmente algunos rayos de luz sern capturados por un sensor (como un ojo
humano, el sensor CCD (Charge-Coupled Device) de una cmara digital o una
pelcula fotogrca).
La luz puede ser modelada empleando diversas aproximaciones, centrndose en
las propiedades direccionales (rayos puramente geomtricos), como ondas electromagnticas o como partculas cunticas (fotones). Dependiendo del mtodo de representacin, suele emplearse un modelo u otro. Independientemente del tratamiento que
demos a la luz, sta debe ser simulada como energa que viaja en el espacio. Las
fuentes de luz sern emisores de esta energa.
Directamente asociado al concepto de iluminacin encontramos las sombras. Gracias a la proyeccin de sombras, podemos establecer relaciones espaciales entre los
objetos de la escena. Por ejemplo, en la Figura 16.1, gracias al uso de sombras podemos saber la posicin exacta de la esfera relativa a la escalera.
Simplica!!
Como ya comentamos en el Captulo 15, este modelo de iluminacin
es una simplicacin del modelo fsicamente correcto que se resuelve con mejores aproximaciones de
la ecuacin de Rendering de James
Kajiya.
16.2.
Fuente
Puntual
En videojuegos suelen permitirse tres tipos de fuentes de luz directamente soportadas por el hardware de aceleracin grco:
Las fuentes puntuales (point lights) irradian energa en todas las direcciones
a partir de un punto que dene su posicin en el espacio. Este tipo de fuentes
permite variar su posicin pero no su direccin. En realidad, las fuentes de luz
puntuales no existen como tal en el mundo fsico (cualquier fuente de luz tiene
asociada un rea, por lo que para realizar simulaciones realistas de la iluminacin tendremos que trabajar con fuentes de rea. Ogre dene este tipo de fuente
como LT_POINT.
Uno de los tipos de fuentes ms sencillo de simular son las denominadas fuentes
direccionales (directional lights), que pueden considerarse fuentes situadas a
una distancia muy grande, por lo que los rayos viajan en una nica direccin
en la escena (son paralelos entre s). El sol podra ser modelado mediante una
fuente de luz direccional. De este modo, la direccin viene determinada por un
vector l (especicado en coordenadas universales). Este tipo de fuentes de luz
no tienen por tanto una posicin asociada (nicamente direccin). Este tipo de
fuente est descrito como LT_DIRECTIONAL en Ogre.
Fuente
Direccional
Fuente Foco
Figura 16.2: Tipos de fuentes de
luz utilizadas en aplicaciones interactivas. Ogre soporta nicamente
estos tres tipos de fuentes bsicos.
[431]
Finalmente los focos (spot lights) son en cierto modo similares a las fuentes
de luz puntuales, pero aadiendo una direccin de emisin. Los focos arrojan
luz en forma cnica o piramidal en una direccin especca. De este modo,
requieren un parmetro de direccin, adems de dos ngulos para denir los
conos de emisin interno y externo. Ogre las dene como LT_SPOTLIGHT.
Las fuentes de luz permiten especicar multitud de parmetros y propiedades, como el color difuso y especular. Una fuente de luz puede denir un color de iluminacin
difuso (como si el cristal de la bombilla estuviera tintado), y un color diferente para el
brillo especular. Ambas propiedades estn directamente relacionadas con el modo en
el que reejarn la luz los materiales.
En aplicaciones de sntesis de imagen realista suelen denirse adems fuentes de
luz de rea. Este tipo de fuentes simulan el comportamiento de la luz de un modo
ms realista, donde potencialmente cada punto de la supercie se comporta como un
emisor de luz. En la seccin 16.7 estudiaremos el modelo de Radiosidad que permite
trabajar con fuentes de luz de rea.
16.3.
En muchas ocasiones se implementan, empleando el Pipeline en GPU programable, algoritmos de sombreado avanzados como aproximaciones al Ambient Occlusion (que veremos en la seccin 16.6 o Radiosidad Instantnea
(ver seccin 16.7).
b
Figura 16.3: Comparativa entre
sombras calculadas por a) Mapas
de Texturas y b) Stencil Buffer. Las
basadas en mapas de texturas son
claramente dependientes de la resolucin del mapa, por lo que deben evitarse con reas de proyeccin muy extensas.
Las tcnicas de tipo Modulative nicamente oscurecen las zonas que quedan en
sombra, sin importar el nmero de fuentes de luz de la escena. Por su parte, si la
tcnica se especica de tipo Additive es necesario realizar el clculo por cada fuente
de luz de modo acumulativo, obteniendo un resultado ms preciso (pero ms costoso
computacionalmente).
Cada tcnica tiene sus ventajas e inconvenientes (como por ejemplo, el relativo a
la resolucin en el caso de los Mapas de Textura, como se muestra en la Figura 16.3).
No es posible utilizar varias tcnicas a la vez, por lo que deberemos elegir la tcnica
que mejor se ajuste a las caractersticas de la escena que queremos representar. En
trminos generales, los mtodos basados en mapas de textura suelen ser ms precisos,
pero requieren el uso de tarjetas aceleradoras ms potentes. Veamos las caractersticas
generales de las tcnicas de sombreado para estudiar a continuacin las caractersticas
particulares de cada mtodo.
C16
[432]
16.3.1.
[433]
Figura 16.4: Utilizacin de diversos modos de clculo de sombras y materiales asociados. a) Sombras
mediante Mapas de Texturas y Material simple. b) Sombras por Stencil Buffer y Material simple. c) Sombras por Stencil Buffer y Material con Ambient Occlusion precalculado. d) Sombras mediante Mapas de
Texturas y Material basado en textura de mrmol procedural precalculada. e) Sombras por Stencil Buffer
y Material basado en textura de mrmol procedural precalculada. f) Sombras por Stencil Buffer y Material
combinado de c) + e) (Ambient Occlusion y Textura de Mrmol precalculados).
Fuente
Foco
Imagen Resultado
+
Volumen de
sombra
proyectada
Cmara
Volumen de
sombra
proyectada
Stencil
buffer
Figura 16.5: Proceso de utilizacin del Stencil Buffer para el recorte de la proyeccin del volumen de
sombra.
C16
[434]
Cmara
Fuente
de luz
b
a
Mapa Profundidad
desde la fuente de luz
Pb
Pa
Resultado final
Empleando el Stencil Buffer, las sombras de objetos transparentes emitirn sombras totalmente slidas. Es posible desactivar totalmente las sombras de este
tipo de objetos, pero empleando Stencil Buffer no es posible obtener sombras
semitransparentes.
Empleando esta tcnica no es posible obtener sombras con aristas suaves. En el
caso de necesitar este tipo de sombras ser necesario emplear la tcnica basada
de Mapas de Textura (ver Seccin 16.3.2).
Esta tcnica permite que el objeto reciba su propia sombra, como se muestra en
la Figura 16.6.b.
Es necesario que Ogre conozca el conjunto de aristas que denen el modelo. Exportando desde Blender, la utilidad OgreXMLConverter realiza el clculo
automticamente. Sin embargo, si implementamos nuestro propio cargador, deberemos llamar a buildEdgeList de la clase Mesh para que construya la lista.
16.3.2.
Z-Fighting
La distincin en grupos de emisores y receptores de sombras se crea
para evitar problemas de Z-ghting
(cuando dos o ms elementos tienen asociada la misma profundidad
en el ZBuffer y su representacin es
incorrecta).
[435]
Este tipo de sombras permiten el manejo correcto de la transparencia. Adems,
las sombras pueden tener un color propio.
Se basan principalmente en el uso de la GPU, por lo que descargan en gran
medida la CPU. Directamente relacionado con esta caracterstica, las sombras
basadas en mapas de textura permiten componer otros efectos en la GPU (como
deformaciones empleando un Vertex Shader).
Esta tcnica no permite que el objeto reciba sus propias sombras.
16.4.
Ejemplo de uso
El siguiente ejemplo de uso construye una aplicacin que permite elegir entre el
uso de texturas basadas en Stencil Buffer o en Mapas de Textura.
Listado 16.1: Fragmento de MyApp.cpp
1 void MyApp::createScene() {
2
_sceneManager->setShadowTechnique(SHADOWTYPE_STENCIL_MODULATIVE);
C16
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 }
[436]
En la lnea 2 se dene la tcnica de clculo de sombras que se utilizar por defecto, aunque
se cambiar en tiempo de ejecucin empleando las
en el FrameListener
teclas 1 y 2 . Las lneas 2-3 denen propiedades generales de la escena, como el
color con el que se representarn las sombras y el color de la luz ambiental (que en
este ejemplo, debido a la denicin de los materiales, tendr especial importancia).
Las lneas 6-9 nicamente tienen relevancia en el caso del uso del mtodo de
clculo basado en mapas de textura. Si
el mtodo utilizado es el de Stencil Buffer
simplemente sern ignoradas. La lnea 6 congura el nmero de texturas que se emplearn para calcular las sombras. Si se dispone de ms de una fuente de luz (como en
este ejemplo), habr que especicar el nmero de texturas a utilizar.
El tamao de la textura (cuadrada) se indica en la lnea 7 . A mayor tamao,
mejor resolucin en la sombra (pero mayor cantidad de memoria gastada). El tamao
por defecto es 512, y debe ser potencia de dos.
A continuacin en las lneas 9-26 se denen dos fuente de luz de tipo SPOTLIGHT.
Cada luz dene su posicin y rotacin.
En el caso de la segunda fuente
de
luz se dene
adems un color difuso (lnea 20 ). El ngulo interno y externo 13-14 dene el cono
de iluminacin de la fuente. En ambas fuentes se ha activado la propiedad de que la
fuente de luz permita el clculo de sombras.
El mtodo de setCastShadows pertenece a la clase MovableObject. En el caso de entidades, si se especica como cierto, el objeto arrojar sombras sobre
otros objetos. En caso contrario, funcionar como un receptor de sombras.
El mismo mtodo se aplica sobre luces (que, es un tipo especco de MovableObject). En el caso de fuentes de luz, el mismo mtodo sirve para especicar si esa fuente de luz se utilizar en el clculo de sombras.
0 ), se compone en dos capas de textura el color base (calculado mediante una textura
procedural) y una capa de iluminacin basada en Ambient Occlusion. La denicin
del material compuesto se muestra en la Figura 16.8. Gracias a la separacin de la
iluminacin y del color en diferentes mapas, es posible cambiar la resolucin individualmente de cada uno de ellos, o reutilizar el mapa de iluminacin en diferentes
modelos que compartan el mismo mapa de iluminacin pero tengan diferente mapa de
color.
Multitud de juegos utilizan el preclculo de la iluminacin. Quake II y III utilizaron modelos de Radiosidad para crear mapas de iluminacin para simular de una
forma mucho ms realista el comportamiento fsico de la luz en la escena. Aunque
actualmente poco a poco se implantan las tcnicas de iluminacin dinmicas a nivel
de pxel, los mapas de iluminacin siguen siendo una opcin ampliamente utilizada.
Figura 16.8: Denicin del material multicapa para componer una
textura con iluminacin de tipo AbmientOcclusion con la textura de
color. Ambas texturas emplean un
despliegue automtico (tipo LightMap en Blender).
Mapa de color
Mapa de
Iluminacin
Resultado Final
(Ambient Occlusion)
Figura 16.9: Ejemplo de uso de un mapa de iluminacin para denir sombras suaves en un objeto. El mapa
de color y el mapa de iluminacin son texturas independiente que se combinan (multiplicando su valor) en
la etapa nal.
16.5.
Mapas de Iluminacin
De este modo, para cada cara poligonal del modelo se denen una o varias capas de
textura. La capa del mapa de iluminacin es nalmente multiplicada con el resultado
de las capas anteriores (como se puede ver en la Figura 16.9).
A diferencia de las texturas de color donde cada vrtice del modelo puede compartir las mismas coordenadas UV con otros vrtices, en mapas de iluminacin cada
vrtice de cada cara debe tener una coordenada nica. Los mapas de iluminacin se
cargan de la misma forma que el resto de texturas de la escena. En la Figura 16.8
hemos estudiado un modo sencillo de composicin de estos mapas de iluminacin,
C16
Geometra base
[437]
[438]
aunque pueden denirse otros mtodos que utilicen varias pasadas y realicen otros
modos de composicin. A continuacin estudiaremos dos tcnicas ampliamente utilizadas en el preclculo de la iluminacin empleando mapas de iluminacin: Ambient
Occlusion y Radiosidad.
Figura 16.10: Despliegue del modelo de Neptuno empleando Lightmap Pack. Cada cara poligonal tiene asociado un espacio propio en la
textura.
Finalmente,
Bake se inicia el proceso de clculo. En la ventana de UV Mapping deber verse la
actualizacin en tiempo real de la textura mapeada al modelo.
[439]
16.6.
Ambient Occlusion
El empleo del trmino de luz ambiente viene aplicndose desde el inicio de los
grcos por computador como un mtodo muy rpido de simular la contribucin de
luz ambiental que proviene de todas las direcciones. La tcnica de Ambient Occlusion
es un caso particular del uso de pruebas de oclusin en entornos con iluminacin local
para determinar los efectos difusos de iluminacin. Estas tcnicas fueron introducidas
inicialmente por Zhurov como alternativa a las tcnicas de radiosidad para aplicaciones interactivas (videojuegos), por su bajo coste computacional.
Punto
de Vista
Objeto
(d(P, ))cosd
(16.1)
Figura 16.12: Descripcin esquemtica del clculo de Ambient Occlusion. a) Esquema de clculo del
valor de ocultacin W (P ). b) Render obtenido con iluminacin lobal
(dos fuentes puntuales). c) La misma conguracin que en b) pero
con Ambient Occlusion de 10 muetras por pxel.
C16
[440]
El mtodo AO Aproximado realiza una rpida aproximacin que, en muchos casos, puede ser suciente. No sufre de ruido de muestreo, por lo que es buena opcin
para ser utilizada en multitud de ocasiones. El parmetro Error dene la calidad de las
sombras calculadas (valores menores implican mejores resultados con mayor tiempo
de cmputo). El checkbox Pixel Cache si est activo hace que el valor de sombreado
se interpole entre pxeles vecinos, haciendo que el clculo sea an ms rpido (aunque
menos exacto). A continuacin estudiaremos algunas opciones relevantes del mtodo:
Falloff: Esta opcin controla el tamao de las sombras calculadas por AO. Si
est activa, aparece un nuevo control Strength que permite variar el factor de
atenuacin. Con valores mayores de Strength, la sombra aparece ms enfocada
(es ms pequea).
Add (Por defecto): El punto recibe luz segn los rayos que no se han chocado
con ningn objeto. La escena esta ms luminosa que la original sin AO.
Multiply: El punto recibe sombra segn los rayos que han chocado con algn
objeto. La escena es ms oscura que la original sin AO.
16.7.
Radiosidad
Figura 16.14: El modelo de Radiosidad permite calcular el intercambio de luz entre supercies difusas,
mostrando un resultado de render
correcto en el problema planteado
en la histrica Cornell Box.
16.7. Radiosidad
[441]
Figura 16.15: Ejemplo de aplicacin del mtodo de radiosidad sobre una escena simple. En el techo de
la habitacin se ha denido una fuente de luz. a) Malla original. b) Malla de radiosidad. c) Resultado del
proceso de renderizado.
En 1988, Cohen introdujo una variante de clculo basada en el renamiento progresivo que permite que la solucin de radiosidad encontrada en cada iteracin del
algoritmo sea mostrada al usuario. Este mtodo progresivo es un mtodo incremental
que requiere menos tiempo de cmputo y de almacenamiento; en cada iteracin se
calcula los factores de forma entre una supercie y el resto (en el artculo original se
requera el clculo de n2 factores de forma).
Este mtodo de renamiento progresivo nalmente obtiene la misma solucin que
el original, proporcionando resultados intermedios que van siendo renados.
Renamiento Progresivo
Blender implementa el mtodo de
Radiosidad basado en renamiento
progresivo
C16
[442]
como una nueva malla poligonal que tiende a desenfocar los lmites de las sombras.
Como en videojuegos suelen emplearse modelos en baja poligonalizacin, y es posible mapear esta iluminacin precalculada en texturas, el modelo de Radiosidad se
ha empleado en diveros ttulos de gran xito comercial. Actualmente incluso existen
motores grcos que calculan soluciones de radiosidad en tiempo real.
2
1
Figura 16.16: Esquema de la matriz de radiosidad. En cada posicin de la matriz se calcula el Factor de
Forma. La diagonal principal de la matriz no se calcula.
16.7. Radiosidad
[443]
Figura 16.19: Opciones generales de Radiosidad (Paneles Radio Render, Radio Tool y Calculation.
Los elementos que emiten luz tendrn el campo Emit del material con un valor
mayor que 0. Los emisores de este ejemplo tienen un valor de emisin de 0.4.
informacin
de radiosidad calculada, la moveremos a otra capa (mediante la tecla M ) para tener
los objetos mejor organizados. Hecho esto, podemos liberar la memoria ocupada por
estos datos pinchando en Free Radio Data.
El resultado de la malla de radiosidad se muestra en la Figura 16.20. Como se puede comprobar, es una malla extremadamente densa, que no es directamente utilizable
en aplicaciones de tiempo real. Vamos a utilizar el Baking de Blender para utilizar esta
informacin de texturas sobre la escena original en baja poligonalizacin.
C16
solucin
Escape , quedndonos con la aproximacin que se ha conseguido hasta ese instante.
Si no estamos satisfechos con la solucin de Radiosidad calculada, podemos eliminarla pinchando en Free Radio Data.
[444]
Figura 16.21: Resultado de aplicacin del mapa de radiosidad al modelo en baja resolucin.
Ahora seleccionamos primero la malla de radiosidad, y despus con Shift pulsado
seleccionamos la malla en baja resolucin. Pinchamos en el botn Selected
to Active
de la pestaa Bake, activamos Full Render y pinchamos en el botn Bake . Con esto
debemos haber asignado el mapa de iluminacin de la malla de radiosidad al objeto
en baja poligonalizacin (ver Figura 16.21).
Captulo
17
asta ahora hemos empleado exportadores genricos de contenido de animacin, mallas poligonales y denicin de texturas. Estas herramientas facilitan
enormemente la tarea de construccin de contenido genrico para nuestros
videojuegos. En este captulo veremos cmo crear nuestros propios scripts de exportacin y su posterior utilizacin en una sencilla demo de ejemplo.
17.1.
Introduccin
445
[446]
La versin de Blender 2.65 utiliza Python 3.3, por lo que ser necesaria su instalacin para ejecutar los scripts desarrollados. En una ventana de tipo Text Editor
podemos editar los scripts. Para su ejecucin,
bastar con situar el puntero del ratn
dentro de la ventana de texto y pulsar Alt P .
1 Resulta especialmente interesante la comparativa emprica de Python con otros lenguajes
de
programacin
disponible
en
http://www.ipd.uka.de/~{}prechelt/
\discretionary{-}{}{}Biblio/jccpprtTR.pdf
17.1. Introduccin
[447]
Aunque la versin 2.65 de Blender permite aadir nuevas propiedades a los objetos de la escena, por mantener la compatibilidad con versiones anteriores lo haremos
codicndolo como parte del nombre. En la pestaa Object, es posible especicar el
nombre del objeto, como se muestra en la Figura 17.3. En nuestro ejemplo, se ha
utilizado el siguiente convenio de nombrado:
Cmaras. Las cmaras codican en el nombre (separadas por guin bajo), como primer parmetro el identicador de la misma (un ndice nico), y como
segundo parmetro el nmero de frames asociado a su animacin. De este modo, la cmara llamada GameCamera_1_350 indica que es la primera cmara
(que ser la utilizada en el estado de Juego), y que tiene una animacin denida
de 350 frames.
el nom
bre del chero XML que se exportar, 9 el nombre del objeto donde sedene el grafo
(como hemos indicado anteriormente ser una malla poligonal), y 10 un valor que
utilizaremos como distancia mxima de separacin entre un vrtice del grafo y cada
Empty de la escena (para considerar que estn asociados).
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
# --- isclose -------------------------------------------------# Decide si un empty coincide con un vertice del grafo
# -------------------------------------------------------------def isclose(empty, coord):
xo, yo, zo = coord
xd, yd, zd = empty.location
v = mathutils.Vector((xo-xd, yo-yd, zo-zd))
if (v.length < EPSILON):
return True
else:
return False
# --- gettype -------------------------------------------------# Devuelve una cadena con el tipo del nodo del grafo
# -------------------------------------------------------------def gettype (dv, key):
obs = [ob for ob in bpy.data.objects if ob.type == EMPTY]
for empty in obs:
empName = empty.name
if ((empName.find("spawn") != -1) or
(empName.find("drain") != -1)):
if (isclose(empty, dv[key])):
return type ="+ empName[:-1] +"
return type=""
C17
[448]
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
ID1
ID2
ID3
ID4
=
=
=
=
*2
*4
*6
*8
graph = bpy.data.objects[GRAPHNAME]
dv = {}
# Diccionario de vertices
for vertex in graph.data.vertices:
dv[vertex.index+1] = vertex.co
de = {}
# Diccionario de aristas
for edge in graph.data.edges:
# Diccionario de aristas
de[edge.index+1] = (edge.vertices[0], edge.vertices[1])
file = open(FILENAME, "w")
std=sys.stdout
sys.stdout=file
print ("<?xml version=1.0 encoding=UTF-8?>\n")
print ("<data>\n")
# --------- Exportacion del grafo --------------------print ("<graph>")
for key in dv.keys():
print (ID1 + <vertex index=" + str(key) + " +
gettype(dv,key) +>)
x,y,z = dv[key]
print (ID2 + <x> %f</x> <y> %f</y> <z> %f</z> % (x,y,z))
print (ID1 + </vertex>)
for key in de.keys():
print (ID1 + <edge>)
v1,v2 = de[key]
print (ID2 + <vertex> %i</vertex> <vertex> %i</vertex>
% (v1,v2))
print (ID1 + </edge>)
print ("</graph>\n")
# --------- Exportacion de la camara ------------------obs = [ob for ob in bpy.data.objects if ob.type == CAMERA]
for camera in obs:
camId = camera.name
camName = camId.split("_")[0]
camIndex = int(camId.split("_")[1])
camFrames = int (camId.split("_")[2])
print (<camera index=" %i" fps=" %i"> %
(camIndex, bpy.data.scenes[Scene].render.fps))
print (ID1 + <path>)
for i in range (camFrames):
cFrame = bpy.data.scenes[Scene].frame_current
bpy.data.scenes[Scene].frame_set(cFrame+1)
x,y,z = camera.matrix_world.translation
qx,qy,qz,qw = camera.matrix_world.to_quaternion()
print (ID2 + <frame index=" %i"> % (i+1))
print (ID3 + <position>)
print (ID4 + <x> %f</x> <y> %f</y> <z> %f</z> % (x,y,z))
print (ID3 + </position>)
print (ID3 + <rotation>)
print (ID4 + <x> %f</x> <y> %f</y> <z> %f</z> <w> %f</w> %
(qx,qy,qz,qw))
print (ID3 + </rotation>)
print (ID2 + </frame>)
print (ID1 + </path>)
print (</camera>)
print ("</data>")
file.close()
sys.stdout = std
17.1. Introduccin
[449]
El proceso de exportacin del XML se realiza directamente al vuelo, sin emplear
las facilidades (como el DOM o SAX de Python). La sencillez del XML descrito, as
como la independencia de una instalacin completa de Python hace que sta sea la
alternativa implementada en otros exportadores (como el exportador de Ogre, o el de
Collada).
En las lneas 44,48 se crean dos diccionarios donde se guardarn las listas relativas a las coordenadas de los vrtices del grafo y las aristas. Blender numera los
vrtices y aristas asociados a una malla poligonal comenzando en 0. En la descripcin
del XML indicamos por convenio que todos los ndices comenzaran en 1, por lo que
es necesario sumar 1 a los ndices que nos devuelve Blender.
La malla asociada al objeto de nombre indicado
en GRAPHNAME
se obtiene me
diante la llamada a bpy.data.objects (lnea 42 ). La lnea 56 simplemente imprime la
cabecera del XML.
En el bloque descrito por 59-73 se generan las entradas relativas a la denicin del
grafo, empleando los diccionarios de vrtices dv y de aristas de creados anteriormente.
En gettype se calcula la cadena correspondiente al tipo del nodo del grafo. Como
hemos indicado anteriormente, el tipo del nodo se calcula segn la distancia a los
objetos Empty de la escena. Si el vrtice est muy cerca de un Empty con subcadena
drain o spawn en su nombre, le asignaremos ese tipo al vrtice
del grafo. En otro
caso, el nodo del grafo ser genrico. De este modo, en la lnea 28 se obtiene en obs
la lista de todos los objetos de tipo Empty de la escena. Para cada uno de estos objetos,
si el nombre contiene alguna de las subcadenas indicadas y est sucientemente
cerca
(empleando la funcin auxiliar isclose), entonces devolvemos
ese
tipo
(lnea
34
) para
el nodo pasado como argumento de la funcin (lnea 27 ). En otro caso, devolvemos
la
vaca para tipo (que indica que no es un nodo especial del grafo) en la lnea
cadena
35 .
La funcin auxiliar isclose (denida en las lneas 15-22 devolver True si el empty
est a una distancia menor que respecto del nodo del grafo. En otro caso, devolver
False. Para realizar esta comparacin, simplemente creamos un vector restando las
coordenadas del vrtice con
la posicin del Empty y utilizamos directamente el atributo de longitud (lnea 19 ) de la clase Vector de la biblioteca mathutils.
La biblioteca Mathutils de Blender contiene una gran cantidad de funciones
para trabajar con Matrices, Vectores, Cuaternios y un largo etctera.
Trayectorias genricas
Aunque en este ejemplo exportaremos trayectorias sencillas, el exportar la posicin de la cmara en cada frame nos permite tener un control absoluto sobre la pose en cada
instante. En la implementacin en
OGRE tendremos que utilizar algn
mecanismo de interpolacin entre
cada pose clave.
La segunda parte del cdigo (denido en las lneas 76-100 ) se encarga de exportar
las animaciones asociadas a las cmaras. Segn el convenio explicado anteriormente,
el propio nombre de la cmara codica el ndice de la cmara
(ver lnea 80 ) y el n
mero de frames que contiene su animacin (ver lnea 81 ). En el caso de este ejemplo
se han denido dos cmaras (ver Figura 17.4). La cmara del men inicial es esttica,
por lo que dene en la parte relativa a los frames un valor de 1 (no hay animacin). La
cmara del juego describir rotaciones siguiendo una trayectoria circular.
C17
Muy Cerca!
En la denicin del XML (ver Captulo 6 del Mdulo 1), un nodo puede tener
asociado un tipo. Para obtener el tipo de cada
nodo utilizamos una funcin auxiliar
llamada gettype (denida en las lneas 27-35 ).
[450]
spawn4
spawn1
drain2
spawn2
drain1
spawn3
Figura 17.4: Denicin del grafo asociado al juego NoEscapeDemo en Blender. En la imagen de la izquierda se han destacado la posicin de los Emptys auxiliares para describir el tipo especial de algunos nodos
(vrtices) del grafo. La imagen de la derecha muestra las dos cmaras de la escena y la ruta descrita para la
cmara principal del juego.
El nmero de frame se
directamente en el atributo frame_current del la
modica
escena actual (ver lnea 86 ). Para cada frame se exporta la posicin de la cmara
(obtenida
en la ltima columna de la matriz de transformacin del objeto, en la lnea
88 ), y el cuaternio de rotacin (mediante una conversin de la matriz en 89 ). La
Figura 17.5 muestra un fragmento del XML exportado relativo a la escena base del
ejemplo. Esta exportacin puede realizarse ejecutando el chero .py desde lnea de
rdenes, llamando a Blender con el parmetro -P <script.py> (siendo script.py el
nombre del script de exportacin).
Figura 17.5: Fragmento del resultado de la exportacin del XML del ejemplo.
Captulo
18
Animacin
Carlos Gonzlez Morcillo
18.1.
Animacin?
El trmino animacin proviene del
griego Anemos, que es la base de la
palabra latina Animus que signica
Dar aliento, dar vida.
Introduccin
451
[452]
Cuando hablamos de tcnicas de animacin por computador nos referimos a sistemas de control del movimiento. Existen multitud de elementos que hacen el problema
del control del movimiento complejo, y de igual forma, existen varias aproximaciones a la resolucin de estas dicultades. La disparidad de aproximaciones y la falta
de unidad en los convenios de nombrado, hacen que la categorizacin de las tcnicas
de animacin por computador sea difcil. Una propuesta de clasicacin de se realiza
segn el nivel de abstraccin.
As, se distingue entre sistemas de animacin de alto nivel, que permiten al animador especicar el movimiento en trminos generales (denen el comportamiento en
trminos de eventos y relaciones), mientras que los sistemas de bajo nivel requieren
que el animador indique los parmetros de movimiento individualmente.
Si se desean animar objetos complejos (como por ejemplo la gura humana), es
necesario, al menos, un control de jerarqua para reducir el nmero de parmetros
que el animador debe especicar. Incluso en personajes digitales sencillos, como el
mostrado en la gura 18.1 el esqueleto interno necesario tiene un grado de complejidad considerable. En concreto, este esqueleto tiene asociados 40 huesos con diversos
modicadores aplicados individualmente.
Al igual que en los lenguajes de programacin de alto nivel, las construcciones
deben nalmente compilarse a instrucciones de bajo nivel. Esto implica que cualquier
descripcin paramtrica de alto nivel debe transformarse en descripciones de salida
de bajo nivel. Este proceso proceso nalizar cuando dispongamos de todos los datos
necesarios para todos los frames de la animacin.
De esta forma, lo que se busca en el mbito de la animacin es ir subiendo de nivel,
obteniendo sistemas de mayor nivel de abstraccin. Las primeras investigaciones comenzaron estudiando las tcnicas de interpolacin de movimiento entre frames clave,
utilizando Splines. Mediante este tipo de curvas, los objetos podan moverse de forma
suave a lo largo de un camino en el espacio. Desde este punto, han aparecido multitud
de tcnicas de animacin de medio nivel (como la animacin basada en scripts, animacin procedural, animacin jerrquica basada en cinemtica directa e inversa y los
estudios de sntesis animacin automtica).
A continuacin estudiaremos los principales niveles de abstraccin relativos a la
denicin de animacin por computador.
Tcnicas bsicas
18.1.1.
Animacin Bsica
Lo fundamental en este nivel es cmo parametrizar los caminos bsicos de movimiento en el espacio. Hay diversas alternativas (no excluyentes) como los sistemas de
Script, sistemas de Frame Clave y animacin dirigida por Splines.
Sistemas de Script. Histricamente, los primeros sistemas de control del movimiento fueron los sistemas basados en scripts. Este tipo de sistemas requieren
que el usuario escriba un guin en un lenguaje especco para animacin y adems, presuponen una habilidad por parte del animador de expresar la animacin
con el lenguaje de script. Este tipo de aproximacin producen animaciones de
baja calidad, dada la complejidad en la especicacin de las acciones.
Sistema de Frame Clave. Los sistemas de Frame Clave toman su nombre de
la jerarqua de produccin tradicional de Walt Disney. En estos sistemas, los
animadores ms experimentados diseaban los fotogramas principales de cada
secuencia. La produccin se completaba con artistas jvenes que aadan los
frames intermedios3 .
3 Realizando
18.1. Introduccin
[453]
Dependiendo del tipo de mtodo que se utilice para el clculo de los fotogramas
intermedios estaremos ante una tcnica de interpolacin u otra. En general, suelen emplearse curvas de interpolacin del tipo Spline, que dependiendo de sus
propiedades de continuidad y el tipo de clculo de los ajustes de tangente obtendremos diferentes resultados. En el ejemplo de la Figura 18.2, se han aadido
claves en los frames 1 y 10 con respecto a la posicin (localizacin) y rotacin
en el eje Z del objeto. El ordenador calcula automticamente la posicin y rotacin de las posiciones intermedias. La gura muestra las posiciones intermedias
calculadas para los frames 4 y 7 de la animacin.
Animacin Procedural. En esta categora, el control sobre el movimiento se
realiza empleando procedimientos que denen explcitamente el movimiento
como funcin del tiempo. La generacin de la animacin se realiza mediante
descripciones fsicas, como por ejemplo en visualizaciones cientcas, simulaciones de uidos, tejidos, etc... Estas tcnicas de animacin procedural sern
igualmente estudiadas a continuacin este mdulo curso. La simulacin dinmica, basada en descripciones matemticas y fsicas suele emplearse en animacin de acciones secundarias. Es habitual contar con animaciones almacenadas
a nivel de vrtice simulando comportamiento fsico en videojuegos. Sera el
equivalente al Baking de animaciones complejas precalculadas.
Con base en estas tcnicas fundamentales se denen los mtodos de animacin de
alto nivel descritos a continuacin.
Dentro de este grupo incluimos la animacin mediante tcnicas cinemticas jerrquicas (como cinemtica directa o inversa de guras articuladas), las modernas
aproximaciones de sntesis automtica de animacin o los sistemas de captura del movimiento. Estas tcnicas especcas sern estudiadas en detalle en el Mdulo 3 del
curso, por lo que nicamente realizaremos aqu una pequea descripcin introductoria.
Cinemtica Directa. Empleando cinemtica directa, el movimiento asociado a
las articulaciones debe ser especicado explcitamente por el animador. En el
ejemplo de la Figura 18.3, la animacin del efector nal vendra determinada indirectamente por la composicin de transformaciones sobre la base, el hombro,
el codo y la mueca. Esto es, una estructura arbrea descendente. Esto es, dado
el conjunto de rotaciones , obtenemos la posicin del efector nal X como
X = f ().
Cinemtica Inversa. El animador dene nicamente la posicin del efector nal. Mediante cinemtica inversa se calcula la posicin y orientacin de todas
las articulaciones de la jerarqua que consiguen esa posicin particular del efector nal mediante = f (X).
Una vez realizada esta introduccin general a las tcnicas de animacin por computador, estudiaremos en la siguiente seccin los tipos de animacin y las caractersticas
generales de cada uno de ellos en el motor grco Ogre.
C18
18.1.2.
[454]
Posiciones Interpoladas
(Frames 4 y 7)
Posicin Clave
(Frame 1)
Posicin Clave
(Frame 10)
Rotacin Z
Posicin X
Posicin Y
Posicin Z
10
Frame
Figura 18.2: Ejemplo de uso de Frames Clave y Splines asociadas a la interpolacin realizada.
18.2.
Animacin en Ogre
Ogre gestiona la posicin de los elementos de la escena en cada frame. Esto signica que la escena es redibujada en cada frame. Ogre no mantiene ningn tipo de
estructuras de datos con el estado anterior de los objetos de la escena. La animacin
se gestiona con dos aproximaciones; una basada en frames clave y otra mediante una
variable (habitualmente se emplear el tiempo) utilizada como controlador. El modo
ms sencillo de crear animaciones es mediante el uso de una herramienta externa y
reproducirlas posteriormente.
Como hemos sealado anteriormente, de modo general Ogre soporta dos modos de
animacin: basada en Frames Clave (Keyframe Animation) y basada en Controladores.
18.2.1.
Animacin Keyframe
Ogre utiliza el trmino pista Track para referirse a un conjunto de datos almacenados en funcin del tiempo. Cada muestra realizada en un determinado instante
de tiempo es un Keyframe. Dependiendo del tipo de Keyframes y el tipo de pista, se
denen diferentes tipos de animaciones.
Las animaciones asociadas a una Entidad se representan en un AnimationState.
Este objeto tiene una serie de propiedades que pueden ser consultadas:
AnimationState
Cada animacin de un AnimationState tiene asociado un nombre nico
mediante el que puede ser accedido.
[455]
Hombro
Posicin
del Efector
Base
Codo
Mueca
Posicin
del Efector
C18
Figura 18.3: Ejemplo de uso de Frames Clave y Splines asociadas a la interpolacin realizada.
[456]
Figura 18.4: Principales ventanas de Blender empleadas en la animacin de objetos. En el centro se encuentran las ventanas del NLA Editor y del Dope Sheet, que se utilizarn para denir acciones que sern
exportadas empleando el script de Exportacin a Ogre.
18.2.2.
Controladores
La animacin basada en frames clave permite animar mallas poligionales. Mediante el uso de controladores se posicionan los nodos, deniendo una funcin. El
controlador permite modicar las propiedades de los nodos empleando un valor que
se calcula en tiempo de ejecucin.
18.3.
Acciones
Las accciones permiten trabajar a
un nivel de abstraccin mayor que
empleando curvas IPO (InterPOlation curve). Es conveniente emplear
esta aproximacin siempre que denamos animaciones complejas.
[457]
Las acciones aparecern en el interfaz del exportador de Ogre, siempre que hayan
sido correctamente creadas y estn asociadas a un hueso de un esqueleto. En la Figura 18.4 se han denido dos acciones llamadas Saltar y Rotar (la accin Rotar est
seleccionada y aparece resaltada en la gura). A continuacin estudiaremos el proceso
de exportacin de animaciones.
Como se ha comentado anteriormente, las animaciones deben exportarse empleando animacin basada en esqueletos. En el caso ms sencillo de animaciones de cuerpo
rgido, bastar con crear un esqueleto de un nico hueso y emparentarlo con el objeto
que queremos animar.
un esqueleto con un nico hueso al objeto que queremos aadir median Aadimos
te Shift A Add/Armature/Single Bone. Ajustamos el extremo superior del hueso para
que tenga un tamao similar al objeto a animar (como se muestra en la Figura 18.5).
A continuacin crearemos una relacin de parentesco entre el objeto y el hueso del
esqueleto, de forma que el objeto sea hijo de el esqueleto.
Este parentesco debe denirse en modo Pose . Con el hueso del esqueleto seleccionado, elegiremos
en la cabecera de la ventana 3D (o bien empleando
el modo
Pose
el atajo de teclado Control TAB ). El hueso deber representarse en color azul en el interfaz de Blender. Con el hueso
en modo pose, ahora seleccionamos primero al caballo,
Shift pulsado seleccionamos el hueso (se deber elegir en color
y a continuacin con
azul), y pulsamos Control P , eligiendo Set Parent to ->Bone. Si ahora desplazamos el
hueso del esqueleto en modo pose, el objeto deber seguirle.
A continuacin creamos un par de animaciones asociadas a este hueso. Recordemos que debemos trabajar en modo Pose
, por lo que siempre el hueso debe estar
seleccionado en color azul en el interfaz de Blender. Comenzaremos deniendo una
accin que se llamar Saltar. Aadiremos los frames clave de la animacin (en este
caso, hemos denido frames clave de LocRot en 1, 21 y 35).
Tras realizar esta operacin, las ventanas de DopeSheet y NLA Editor mostrarn
permite modicar
un aspecto como el de la Figura 18.7. La ventana DopeSheet
fcilmente las posiciones de los frames clave asociados al hueso. Pinchando y desplazando los rombos asociados a cada clave, podemos modicar el timing de la anipodemos crear una accin (Action Strip), pinchando
macin. En la ventana NLA
sobre el icono del copo de nieve . Tras esta operacin, se habr creado una accin
denida mediante una barra amarilla.
Es posible indicar
el nombre de la accin en el campo Active Strip, accesible pul
sando la tecla N . Aparecer un nuevo subpanel, como se muestra en la Figura 18.8,
en la zona derecha de la ventana, donde se podrn congurar los parmetros asociados
a la accin (nombre, frames, mtodo de mezclado con otras acciones, etc...).
C18
[458]
Mediante el atajo de teclado Alt A podemos reproducir la animacin relativa a la
accin que tenemos seleccionada. Resulta muy cmodo emplear una ventana de tipo
para denir el intervalo sobre el que se crearn las animaciones.
Timeline
A la hora de exportar las animaciones, procedemos como vimos en el Captulo
11. En File/ Export/ Ogre3D, accederemos al exportador de Blender a Ogre3D. Es
interesante activar las siguientes opciones de exportacin:
Armature Animation. Crea el archivo con extensin .skeleton, que contiene
la denicin jerrquica del conjunto de huesos, junto con su posicin y orientacin. Esta jerarqua incluye la inuencia que tiene cada hueso sobre los vrtices
del modelo poligonal.
Independent Animations. Exporta cada accin de forma independiente, de
modo que no tienen inuencia combinada.
El uso de las animaciones exportadas en Ogre es relativamente sencillo. En siguiente cdigo muestra un ejemplode aplicacin
de las animaciones previamente ex
A
o
Z
se
reproduce
la animacin Saltar o Rotar
portadas. Cuando se pulsa la tecla
hasta que naliza. En la lnea 18 se actualiza el tiempo transcurrido desde la ltima
actualizacin. Cuando se pulsa alguna de las
anteriores, la animacin seleccio teclas
1 if (_keyboard->isKeyDown(OIS::KC_A) ||
2
_keyboard->isKeyDown(OIS::KC_Z)) {
3
if (_keyboard->isKeyDown(OIS::KC_A))
4
_animState = _sceneManager->getEntity("Horse")->
5
getAnimationState("Saltar");
6
else _animState = _sceneManager->getEntity("Horse")->
7
getAnimationState("Rotar");
8
_animState->setEnabled(true);
9
_animState->setLoop(true);
10
_animState->setTimePosition(0.0);
11 }
12
13 if (_animState != NULL) {
14
if (_animState->hasEnded()) {
15
_animState->setTimePosition(0.0);
16
_animState->setEnabled(false);
17
}
18
else _animState->addTime(deltaT);
Huesos y vrtices
En este captulo trabajaremos con
huesos que tienen una inuencia total sobre el objeto (es decir, inuencia de 1.0 sobre todos los vrtices
del modelo).
[459]
19 }
del FrameListener (lnea 3-8 ). Empleando las mismas teclas que en el ejemplo anterior, se resetean
cada
una de ellas, y se actualizan ambas de forma independiente (en
las lneas 26 y 28 ). Ogre se encarga de mezclar sus resultados
(incluso es posible
aplicar transformaciones a nivel de nodo, pulsando la tecla R mientras se reproducen
las animaciones del AnimationState).
1 MyFrameListener::MyFrameListener() {
2
// ...
3
_animState = _sceneManager->getEntity("Horse")->
4
getAnimationState("Saltar");
5
_animState->setEnabled(false);
6
_animState2 = _sceneManager->getEntity("Horse")->
7
getAnimationState("Rotar");
8
_animState2->setEnabled(false);
9 }
10
11 bool MyFrameListener::frameStarted(const FrameEvent& evt) {
12
// ...
13
if (_keyboard->isKeyDown(OIS::KC_A)) {
14
_animState->setTimePosition(0.0);
15
_animState->setEnabled(true);
16
_animState->setLoop(false);
17
}
18
19
if (_keyboard->isKeyDown(OIS::KC_Z)) {
20
_animState2->setTimePosition(0.0);
21
_animState2->setEnabled(true);
22
_animState2->setLoop(false);
23
}
24
25
if (_animState->getEnabled() && !_animState->hasEnded())
26
_animState->addTime(deltaT);
27
if (_animState2->getEnabled() && !_animState2->hasEnded())
28
_animState2->addTime(deltaT);
29
// ...
Obviamente, ser necesario contar con alguna clase de nivel superior que nos gestione las animaciones, y se encarge de gestionar los AnimationState, actualizndolos
adecuadamente.
18.4.
Mezclado de animaciones
C18
[460]
Las variables pblicas (denidas en 16-18 ) contienen el tiempo restante de reproduccin de la pista actual, la duracin (en segundos) del mezclado entre pistas y un
valor booleano que indica si la reproduccin ha nalizado.
La clase incorpora, adems de los tres mtodos principales que estudiaremos a
continuacin, una serie de mtodos auxiliares
que nos permiten obtener valores relativos a las variables privadas (lneas 24-27 ).
Listado 18.3: AminationBlender.h
1 class AnimationBlender {
2
public:
3
enum BlendingTransition {
4
Switch, // Parar fuente y reproduce destino
5
Blend
// Cross fade (Mezclado suave)
6
};
7
8
private:
9
Entity *mEntity;
// Entidad a animar
10
AnimationState *mSource;
// Animacion inicio
11
AnimationState *mTarget;
// Animacion destino
12
BlendingTransition mTransition; // Tipo de transicion
13
bool mLoop;
// Animacion en bucle?
14
15
public:
16
Real mTimeleft;
// Tiempo restante de la animacion (segundos)
17
Real mDuration;
// Tiempo invertido en el mezclado (segundos)
18
bool mComplete;
// Ha finalizado la animacion?
19
20
AnimationBlender( Entity *);
21
void blend(const String &anim, BlendingTransition transition,
22
Real duration, bool l=true);
23
void addTime(Real);
24
Real getProgress() { return mTimeleft/mDuration; }
25
AnimationState *getSource() { return mSource; }
26
AnimationState *getTarget() { return mTarget; }
27
bool getLoop() { return mLoop; }
28 };
4 Ms informacin sobre cmo se implement el motor de mezclado de animaciones del MechWarrior
en http://www.gamasutra.com/view/feature/3456/
Figura 18.9: La clase de AnimationBlender (Mezclado de Animaciones) no tiene nada que ver con
el prestigioso paquete de animacin
del mismo nombre.
[461]
Veamos
20-23 del listado anterior.
Listado 18.4: AminationBlender.cpp (Constructor)
La transicin de tipo Blend implementa una transicin simple lineal de tipo Cross
Fade. En la Figura 18.10 representa este tipo de transicin, donde el primer canal de
animacin se mezclar de forma suave con el segundo, empleando una combinacin
lineal de pesos.
Saltar
Saltar
Saltar
Rotar +
Rotar
Duracin de
la transicin
C18
todos los AnimationState asociados (lnea 7 ), y reseteamos la posicin
de reproduc
cin y su peso asociado para su posterior composicin (lneas 8-9 ).
La clase AnimationBlender dispone de un mtodo principal para cargar animaciones, indicando (como segundo parmetro) el tipo de mezcla que quiere realiarse con
la animacin que est reproducindose. La implementacin actual de la clase admite
dos modos de mezclado; un efecto de mezclado suave tipo cross fade y una transicin
bsica de intercambio de canales.
[462]
27
}
28 }
Saltar
Rotar
Saltar
Rotar
la transicin, se calcular el peso que se asignar a cada canal (lneas 19-22 ). El peso
se calcula empleando una sencilla combinacin lineal, de modo que el peso total para
ambos canales en cualquier instante debe ser igual a uno (ver Figura 18.12).
No es necesario que el peso nal combinado de todas las animaciones sea igual a
uno. No obstante, es conveniente que la suma de todos los canales de animacin estn
normalizados y sean igual a 1.
Cuntas animaciones?
La cantidad de animaciones necesarias para aplicar correctamente
las tcnicas de Animation Blending
pueden ser muy elevadas. Por ejemplo, en MechWarrior de Microsoft
cada robot tena asociadas ms de
150 animaciones.
[463]
En el caso de asignar pesos negativos, la animacin se reproducir empleando
curvas de animacin invertidas.
La composicin de animaciones suele ser una tarea compleja. En el ejemplo desarrollado para ilustrar el uso de la clase AnimationBlender se han utilizado nicamente
dos animaciones, y se han utilizado tiempos de transicin jos en todos los casos.
En produccin es importante trabajar las transiciones y las curvas de animacin para
obtener resultados atractivos.
C18
Figura 18.13: Ejemplo de aplicacin que utiliza la clase AnimationBlender. Mediante las teclas indicadas
en el interfaz es posible componer las animaciones exportadas empleandos los dos modos de transicin
implementados.
Es importante realizar pruebas de la correcta composicin y mezclado de animaciones en las primeras etapas del desarrollo del juego. De otra forma, podemos sufrir desagradables sorpresas cuando se acerca la deadline del proyecto.
Puede resultar complicado ajustar en cdigo la mezcla de animaciones, por
lo que suelen desarrollarse scripts de exportacin adicionales para indicar los
puntos adecuados en cada animacin donde puede componerse con el resto.
[464]
El siguiente listado muestra un ejemplo de uso de la clase. En el constructor del
FrameListener se crea una variable miembro de la clase que contendr un puntero al
objeto AnimBlender.
Cada vez que se actualiza el frame, llamamos al mtodo addTime (ver lnea 11 ) que actualizar los canales de animacin convenientes. En este
ejemplo se han utilizado las 8 teclas descritas en la Figura 18.13 para mezclar las dos
animaciones exportadas en Blender al inicio del captulo.
Listado 18.7: Uso de AnimationBlender
1
2
3
4
5
6
7
8
9
10
11
if (_keyboard->isKeyDown(OIS::KC_Q))
_animBlender->blend("Saltar",AnimationBlender::Blend,0.5, false);
if (_keyboard->isKeyDown(OIS::KC_R))
_animBlender->blend("Rotar", AnimationBlender::Blend, 0.5, true);
if (_keyboard->isKeyDown(OIS::KC_S))
_animBlender->blend("Saltar", AnimationBlender::Switch, 0, true);
if (_keyboard->isKeyDown(OIS::KC_D))
_animBlender->blend("Rotar", AnimationBlender::Switch, 0, false);
_animBlender->addTime(deltaT);
En este captulo hemos estudiado los usos fundamentales de la animacin de cuerpo rgido. En el mdulo 3 del curso estudiaremos algunos aspectos avanzados, como
la animacin de personajes empleando esqueletos y aproximaciones de cinemtica
inversa y el uso de motores de simulacin fsica para obtener, de forma automtica,
animaciones realistas.
Captulo
19
Simulacin Fsica
Carlos Gonzlez Morcillo
n prcticamente cualquier videojuego (tanto 2D como 3D) es necesaria la deteccin de colisiones y, en muchos casos, la simulacin realista de dinmica
de cuerpo rgido. En este captulo estudiaremos la relacin existente entre sistemas de deteccin de colisiones y sistemas de simulacin fsica, y veremos algunos
ejemplos de uso del motor de simulacin fsica libre Bullet.
19.1.
Introduccin
La mayora de los videojuegos requieren en mayor o menor medida el uso de tcnicas de deteccin de colisiones y simulacin fsica. Desde un videojuego clsico como Arkanoid, hasta modernos juegos automovilsticos como Gran Turismo requieren
denir la interaccin de los elementos en el mundo fsico.
Figura 19.1: Anarkanoid, el machacaladrillos sin reglas es un juego tipo Breakout donde la simulacin fsica se reduce a una deteccin de colisiones 2D.
Cuerpo Rgido
Denimos un cuerpo rgido como
un objeto slido ideal, innitamente
duro y no deformable.
465
[466]
La mayor parte de los videojuegos actuales incorporan ciertos elementos de simulacin fsica bsicos. Algunos ttulos se animan a incorporar ciertos elementos
complejos como simulacin de telas, cuerdas, pelo o uidos. Algunos elementos de
simulacin fsica son precalculados y almacenados como animaciones estticas, mientras que otros necesitan ser calculados en tiempo real para conseguir una integracin
adecuada.
Como hemos indicado anteriormente, las tres tareas principales que deben estar
soportadas por un motor de simulacin fsica son la deteccin de colisiones, su resolucin (junto con otras restricciones de los objetos) y calcular la actualizacin del
mundo tras esas interacciones. De forma general, las caractersticas que suelen estar
presentes en motores de simulacin fsica son:
Deteccin de colisiones entre objetos dinmicos de la escena. Esta deteccin
podr ser utilizada posteriormente por el mdulo de simulacin dinmica.
Clculo de lineas de visin y tiro parablico, para la simulacin del lanzamiento
de proyectiles en el juego.
Denicin de geometra esttica de la escena (cuerpos de colisin) que formen
el escenario del videojuego. Este tipo de geometra puede ser ms compleja que
la geometra de cuerpos dinmicos.
Especicacin de fuerzas (tales como viento, rozamiento, gravedad, etc...), que
aadirn realismo al videjuego.
Simulacin de destruccin de objetos: paredes y objetos del escenario.
Denicin de diversos tipos de articulaciones, tanto en elementos del escenario
(bisagras en puertas, rales...) como en la descripcin de las articulaciones de
personajes.
Especicacin de diversos tipos de motores y elementos generadores de fuerzas,
as como simulacin de elementos de suspensin y muelles.
Simulacin de uidos, telas y cuerpos blandos (ver Figura 19.2).
19.1.1.
19.1. Introduccin
[467]
PhysX. Este motor privativo, es actualmente mantenido por NVidia con aceleracin basada en hardware (mediante unidades especcas de procesamiento
fsico PPUs Physics Processing Units o mediante ncleos CUDA. Las tarjetas
grcas con soporte de CUDA (siempre que tengan al menos 32 ncleos CUDA) pueden realizar la simulacin fsica en GPU. Este motor puede ejecutarse
en multitud de plataformas como PC (GNU/Linux, Windows y Mac), PlayStation 3, Xbox y Wii. El SDK es gratuito, tanto para proyectos comerciales como
no comerciales. Existen multitud de videojuegos comerciales que utilizan este
motor de simulacin. Gracias al wrapper NxOgre se puede utilizar este motor
en Ogre.
Fsica
Grficos
Lgica del
Juego
Networking
GUI
IA
Figura 19.5: El motor de simulacin fsica est directamente relacionado con otros mdulos del videojuego. Esta dependencia conlleva una serie de dicultades que deben tenerse en cuenta.
19.1.2.
Aspectos destacables
C19
...
Havok. El motor Havok se ha convertido en el estndar de facto en el mundo del software privativo, con una amplia gama de caractersticas soportadas y
plataformas de publicacin (PC, Videoconsolas y Smartphones). Desde que en
2007 Intel comprara la compaa que originalmente lo desarroll, Havok ha sido el sistema elegido por ms de 150 videojuegos comerciales de primera lnea.
Ttulos como Age of Empires, Killzone 2 & 3, Portal 2 o Uncharted 3 avalan la
calidad del motor.
[468]
Figura 19.6: Gracias al uso de PhysX, las baldosas del suelo en Batman Arkham Asylum pueden ser destruidas (derecha). En la imagen de la izquierda, sin usar PhysX el suelo permanece inalterado, restando
realismo y espectacularidad a la dinmica del juego.
Exportacin. La denicin de objetos con propiedades fsicas aade nuevas variables y constantes que deben ser tratadas por las herramientas de exportacin
de los datos del juego. En muchas ocasiones es necesario adems la exportacin
de diferentes versiones de un mismo objeto (una versin de alta poligonalizacin, la versin de colisin, una versin destructible, etc).
Interfaz de Usuario. Es necesario disear interfaces de usuario adaptados a
las capacidades fsicas del motor (cmo se especica la fuerza y la direccin
de lanzamiento de una granada?, de qu forma se interacta con objetos que
pueden recogerse del suelo?).
19.1.3.
Conceptos Bsicos
A principios del siglo XVII, Isaac Netwon public las tres leyes fundamentales
del movimiento. A partir de estos tres principios se explican la mayor parte de los
problemas de dinmica relativos al movimiento de cuerpos y forman la base de la
mecnica clsica. Las tres leyes pueden resumirse como:
1. Un cuerpo tiende a mantenerse en reposo o a continuar movindose en lnea
recta a una velocidad constante a menos que acte sobre l una fuerza externa.
Esta ley resume el concepto de inercia.
2. El cambio de movimiento es proporcional a la fuerza motriz aplicada y ocurre
segn la lnea recta a lo largo de la que se aplica dicha fuerza.
3. Para cada fuerza que acta sobre un cuerpo ocurre una reaccin igual y contraria. De este modo, las acciones mutuas de dos cuerpos siempre son iguales y
dirigidas en sentido opuesto.
En el estudio de la dinmica resulta especialmente interesante la segunda ley de
Newton, que puede ser escrita como
F =ma
(19.1)
donde F es la fuerza resultante que acta sobre un cuerpo de masa m, y con una
aceleracin lineal a aplicada sobre el centro de gravedad del cuerpo.
[469]
1 Proceso
Fsico
(Ecuaciones)
Algoritmo
de Simulacin
Programa
(Implementacin)
Simulacin
(Ejecucin)
zi
zi-1
ac
Enl
e i-2
19.2.
i-1i
xx
i-1i
Test de Interseccin
En realidad, el Sistema de Deteccin de Colisiones puede entenderse como un mdulo para realizar
pruebas de interseccin complejas.
C19
[470]
19.2.1.
Formas de Colisin
Algunos motores de simulacin fsica permiten compartir la misma descripcin de la forma de colisin entre entidades. Esto resulta especialmente til
en juegos donde la forma de colisin es compleja, como en simuladores de
carreras de coches.
(a)
Como se muestra en la Figura 19.10, las entidades del juego pueden tener diferentes formas de colisin, o incluso pueden compartir varias primitivas bsicas (para
representar por ejemplo cada parte de la articulacin de un brazo robtico).
El Mundo Fsico sobre el que se ejecuta el SDC mantiene una lista de todas las
entidades que pueden colisionar empleando habitualmente una estructura global Singleton. Este Mundo Fsico es una representacin del mundo del juego que mantiene
la informacin necesaria para la deteccin de las colisiones. Esta separacin evita que
el SDC tenga que acceder a estructuras de datos que no son necesarias para el clculo
de la colisin.
(b)
Los SDC mantienen estructuras de datos especcas para manejar las colisiones,
proporcionando informacin sobre la naturaleza del contacto, que contiene la lista de
las formas que estn intersectando, su velocidad, etc...
Para gestionar de un modo ms eciente las colisiones, las formas que suelen utilizarse con convexas. Una forma convexa es aquella en la que un rayo que surja desde
su interior atravesar la superce una nica vez. Las supercies convexas son mucho
ms simples y requieren menor capacidad computacional para calcular colisiones que
las formas cncavas. Algunas de las primitivas soportadas habitualmente en SDC son:
Esferas. Son las primitivas ms simples y ecientes; basta con denir su centro
y radio (uso de un vector de 4 elementos).
Cajas. Por cuestiones de eciencia, se suelen emplear cajas lmite alineadas con
los ejes del sistema de coordenadas (AABB o Axis Aligned Bounding Box). Las
cajas AABB se denen mediante las coordenadas de dos extremos opuestos. El
principal problema de las cajas AABB es que, para resultar ecientes, requieren estar alineadas con los ejes del sistema de coordenas global. Esto implica
(c)
Figura 19.10: Diferentes formas de
colisin para el objeto de la imagen.
(a) Aproximacin mediante una caja. (b) Aproximacin mediante un
volumen convexo. (c) Aproximacin basada en la combinacin de
varias primitivas de tipo cilndrico.
Formas de colisin
A cada cuerpo dinmico se le asocia habitualmente una nica forma
de colisin en el SDC.
[471]
Figura 19.11: Gestin de la forma de un objeto empleando cajas lmite alineadas con el sistema de referencia universal AABBs. Como se muestra en la gura, la caja central realiza una aproximacin de la forma
del objeto muy pobre.
C19
[472]
19.2.2.
Optimizaciones
La deteccin de colisiones es, en general, una tarea que requiere el uso intensivo de
la CPU. Por un lado, los clculos necesarios para determianr si dos formas intersecan
no son triviales. Por otro lado, muchos juegos requiren un alto nmero de objetos en
la escena, de modo que el nmero de test de interseccin a calcular rpidamente crece.
En el caso de n objetos, si empleamos un algoritmo de fuerza bruta tendramos una
complejidad O(n2 ). Es posible utilizar ciertos tipos de optimizaciones que mejoran
esta complejidad inicial:
Coherencia Temporal. Este tipo de tcnica de optimizacin (tambin llamada
coherencia entre frames), evita recalcular cierto tipo de informacin en cada
frame, ya que entre pequeos intervalos de tiempo los objetos mantienen las
posiciones y orientaciones en valores muy similares.
Particionamiento Espacial. El uso de estructuras de datos de particionamiento
especial permite comprobar rpidamente si dos objetos podran estar intersecando si comparten la misma celda de la estructura de datos. Algunos esquemas de
particionamiento jerrquico, como rboles octales, BSPs o rboles-kd permiten
optimizar la deteccin de colisiones en el espacio. Estos esquemas tienen en comn que el esquema de particionamiento comienza realizando una subdivisin
general en la raz, llegando a divisiones ms nas y especcas en las hojas. Los
objetos que se encuentran en una determinada rama de la estructura no pueden
estar colisionando con los objetos que se encuentran en otra rama distinta.
Figura 19.13: Bullet soporta la denicin de mallas poligonales animadas. En este ejemplo de las demos ociales, el suelo est animado y los objetos convexos colisionan respondiendo a su movimiento.
19.2.3.
a)
b)
Preguntando al sistema...
En el mdulo 2 ya estudiamos algunas de las funcionalidades que se pueden encontrar en sistemas de deteccin de colisiones. El objetivo es poder obtener resultados
a ciertas consultas sobre el primer objeto que intersecar con un determinado rayo, si
hay objetos situados en el interior de un determinado volumen, etc.
A continuacin veremos dos de las principales collision queries que pueden encontrarse habitualmente en un SDC:
Ray Casting. Este tipo de query requiere que se especique un rayo, y un origen. Si el rayo interseca con algn objeto, se devolver un punto o una lista de
puntos. Como vimos, el rayo se especica habitualmente mediante una ecuacin paramtrica de modo que p(t) = po + td, siendo t el parmetro que toma
[473]
Po
Po
a)
b)
Po
c)
Po
d)
Figura 19.15: Clculo de los puntos de colisin empleando Shape Casting. a) La forma inicialmente se
encuentra colisionando con algn objeto de la escena. b) El SDC obtiene un punto de colisin. c) La forma
interseca con dos objetos a la vez. d) En este caso el sistema calcula todos los puntos de colisin a lo largo
de la trayectoria en la direccin de d.
valores entre 0 y 1. d nos dene el vector direccin del rayo, que determinar la
distancia mxima de clculo de la colisin. Po nos dene el punto de origen del
rayo. Este valor de t que nos devuelve la query puede ser fcilmente convertido
al punto de colisin en el espacio 3D.
Shape Casting. El Shape Casting permite determinar los puntos de colisin de
una forma que viaja en la direccin de un vector determinado. Es similar al Ray
Casting, pero en este caso es necesario tener en cuenta dos posibles situaciones
que pueden ocurrir:
1. La forma sobre la que aplicamos Shape Casting est inicialmente intersecando con al menos un objeto que evita que se desplace desde su posicin
inicial. En este caso el SDC devolver los puntos de contacto que pueden
estar situados sobre la supercie o en el interior del volumen.
2. La forma no interseca sobre ningn objeto, por lo que puede desplazarse
libremente por la escena. En este caso, el resultado de la colisin suele ser
un punto de colisin situado a una determinada distancia del origen, aunque puede darse el caso de varias colisiones simultneas (como se muestra
en la Figura 19.15). En muchos casos, los SDC nicamente devuelven el
resultado de la primera colisin (una lista de estructuras que contienen el
valor de t, el identicador del objeto con el que han colisionado, el punto
de contacto, el vector normal de la supercie en ese punto de contacto, y
algunos campos extra de informacin adicional).
Lista de colisiones
Para ciertas aplicaciones puede ser
igualmente conveniente obtener la
lista de todos los objetos con los que
se interseca a lo largo de la trayectoria, como se muestra en la Figura 19.15.d).
19.3.
La simulacin del movimiento de los objetos del juego forma parte del estudio
de cmo las fuerzas afectan al comportamiento de los objetos. El mdulo del motor
de simulacin que se encarga de la dinmica de los objetos estudia cmo cambian
su posicin en el tiempo. Hasta hace pocos aos, los motores de simulacin fsica se
centraban en estudiar exclusivamente la dinmica de cuerpos rgidos1 , que permite
simplicar el clculo mediante dos suposiciones:
Los objetos en la simulacin obedecen las leyes del movimiento de Newton
(estudiadas en la Seccin 19.1.3). As, no se tienen en cuenta ningn tipo de
efecto cuntico ni relativista.
1 Aunque en la actualidad se encuentran soportadas de una forma muy eciente otras tcnicas, como se
muestran en la Figura 19.16
C19
Uso de Rayos
[474]
Figura 19.16: Ejemplo de simulacin de cuerpos blandos con Bullet. a) Uso de softbodies con formas
convexas. b) Simulacin de una tela sostenida por cuatro puntos. c) Simulacin de cuerdas.
19.4.
Restricciones
[475]
Restriccin
Punto a
Punto
Restriccin
Muelle
Rgido
Articulacin
Bola
Articulacin
Bisagra
Articulacin
Prismtica
Pistn
Figura 19.17: Representacin de algunas de las principales restricciones que pueden encontrarse en los
motores de simulacin fsica.
Muelle rgido. Un muelle rgido (Stiff Spring) funciona como una restriccin
de tipo punto a punto salvo por el hecho de que los objetos estn separados por
una determinada distancia. As, puede verse como unidos por una barra rgida
que los separa una distancia ja.
Bisagras. Este tipo de restriccin limitan el movimiento de rotacin en un determinado eje (ver Figura 19.17). Este tipo de restricciones pueden denir adems
un lmite de rotacin angular permitido entre los objetos (para evitar, por ejemplo, que el codo de un brazo robtico adopte una posicin imposible).
Bolas. Estas restricciones permiten denir lmites de rotacin exibles, estableciendo puntos de anclaje. Sirven por ejemplo para modelar la rotacin que
ocurre en el hombro de un personaje.
Otras restricciones. Cada motor permite una serie de restricciones especcas,
como planares (que restringen el movimiento en un plano 2D), cuerdas, cadenas
de objetos, rag dolls (denidas como una coleccin de cuerpos rgidos conectados utilizando una estructura jerrquica), etc...
19.5.
Introduccin a Bullet
Como se ha comentado al inicio del captulo, Bullet es una biblioteca de simulacin fsica muy utilizada, tanto en la industria del videojuego, como en la sntesis de
imagen realista. Algunas de sus caractersticas principales son:
Est desarrollada ntegramente en C++, y ha sido diseada de modo que tenga
el menor nmero de dependencias externas posibles.
Se distribuye bajo licencia Zlib (licencia libre compatible con GPL), y ha sido
utilizada en proyectos profesionales en multitud de plataformas, entre las que
destacan PlayStation 3, XBox 360, Wii, Linux, Windows, MacOSX, iPhone y
Android.
C19
Pistones. Este tipo de restricciones, denominadas en general restricciones prismticas, permiten limitar el movimiento de traslacin a un nico eje. Estas restricciones podran permitir opcionalmente rotacin sobre ese eje.
[476]
Existen multitud de videojuegos comerciales que han utilizado Bullet como motor
de simulacin fsica. Entre otros se pueden destacar Grand Theft Auto IV (de Red
Dead Redemption), Free Realms (de Sony), HotWheels (de BattleForce), Blood Drive
(de Activision) o Toy Story 3 (The Game) (de Disney).
De igual forma, el motor se ha utilizado en pelculas profesionales, como Hancock
(de Sony Pictures), Bolt de Walt Disney, Sherlock Holmes (de Framestore) o Shrek 4
(de DreamWorks).
Muchos motores grcos y de videojuegos permiten utilizar Bullet. La comunidad
ha desarrollado multitud de wrappers para facilitar la integracin en Ogre, Crystal
Space, Irrlich y Blitz3D entre otros.
En el diseo de Bullet se prest especial atencin en conseguir un motor fcilmente
adaptable y modular. Tal y como se comenta en su manual de usuario, el desarrollador
puede utilizar nicamente aquellos mdulos que necesite (ver Figura 19.19):
Utilizacin exclusiva del componente de deteccin de colisiones.
Utilizacin del componente de dinmicas de cuerpo rgido sin emplear los componentes de SoftBody.
Utilizacin de pequeos fragmentos de cdigo de la biblioteca.
Extensin de la biblioteca para las necesidades especcas de un proyecto.
Bullet dene el pipeline de procesamiento fsico como se muestra en la Figura 19.20. El pipeline se ejecuta desde la izquierda a la derecha, comenzando por la
etapa de clculo de la gravedad, y nalizando con la integracin de las posiciones
(actualizando la transformacin del mundo). Cada vez que se calcula un paso de la
simulacin stepSimulation en el mundo, se ejecutan las 7 etapas denidas en la
imagen anterior.
El Pipeline comienza aplicando la fuerza gravedad a los objetos de la escena.
Posteriormente se realiza una prediccin de las transformaciones calculando la posicin actual de los objetos. Hecho esto se calculan las cajas AABBs, que darn una
estimacin rpida de la posicin de los objetos. Con estas cajas se determinan los
pares, que consiste en calcular si las cajas AABBs se solapan en algn eje. Si no hay
Dinmica
SoftBody
Bullet
MultiHilo
Dinmica
Cuerpo Rgido
Plugins
19.5.1.
Deteccin de Colisiones
Contenedores, Gestin de
Memoria, Bib. Matemtica...
Inicio
[477]
Time Step
Time Step
Dinmica
Aplicar
Gravedad
Formas
Colisin
Time Step
Time Step
Deteccin de Colisiones
Predecir
Transf.
Calcular
AABBs
AABBs
Objetos
Detectar
Pares
Pares
Superpuestos
Puntos
Contacto
Puntos
Contacto
Fin
Time Step
Dinmica
Definir
Restricciones
Velocidades
Transformadas
Datos de Colisin
Resolver
Restricciones
Masa
Inercia
Integrar
Posicin
Restricciones
Articulac.
Datos de Dinmicas
Figura 19.20: Pipeline de Bullet. En la parte superior de la imagen se describen las principales etapas
computacionales, y en la parte inferior las estructuras de datos ms importantes.
Tipos bsicos
Bullet cuenta con un subconjunto
de utilidades matemticas bsicas
con tipos de datos y operadores denidos como btScalar (escalar en
punto otante), btVector3 (vector en
el espacio 3D), btTransform (transformacin afn 3D), btQuaternion,
btMatrix3x3...
Las estructuras de datos bsicas que dene Bullet se dividen en dos grupos: los datos de colisin, que dependen nicamente de la forma del objeto (no se tiene en cuenta
las propiedades fscas, como la masa o la velocidad asociada al objeto), y los datos
de propiedades dinmicas que almacenan las propiedades fsicas como la masa, la
inercia, las restricciones y las articulaciones.
En los datos de colisin se distinguen las formas de colisin, las cajas AABBs, los
pares superpuestos que mantiene una lista de parejas de cajas AABB que se solapan
en algn eje, y los puntos de contacto que han sido calculados como resultado de la
colisin.
19.5.2.
Para comenzar, estudiaremos el Hola Mundo en Bullet, que denir una esfera
que cae sobre un plano. Primero instalaremos las bibliotecas, que pueden descargarse
de la pgina web del proyecto2 . Desde el directorio donde tengamos el cdigo descomprimido, ejecutamos:
cmake . -G "Unix Makefiles" -DINSTALL_LIBS=ON
make
sudo make install
C19
ningn solape, podemos armar que no hay colisin. De otra forma, hay que realizar
un estudio ms detallado del caso. Si hay posibilidad de contacto, se pasa a la siguiente etapa de calcular los contactos, que calcula utilizando la forma de colisin real del
objeto el punto de contacto exacto. Estos puntos de contacto se pasan a la ltima etapa
de clculo de la dinmica, que comienza con la resolucin de las restricciones, donde se determina la respuesta a la colisin empleando las restricciones de movimientos
que se han denido en la escena. Finalmente se integran las posiciones de los objetos,
obteniendo las posiciones y orientaciones nuevas para este paso de simulacin.
[478]
collisionConfiguration);
btSequentialImpulseConstraintSolver* solver = new
btSequentialImpulseConstraintSolver;
btDiscreteDynamicsWorld* dynamicsWorld = new
btDiscreteDynamicsWorld(dispatcher,broadphase,solver,
collisionConfiguration);
[479]
Listado 19.2: Makele para hello world.
1
2
3
4
5
6
7
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
Altura:
...
49.9972
49.9917
49.9833
49.9722
49.9583
49.9417
49.9222
49.9
49.875
49.8472
49.8167
49.7833
49.7472
49.7083
49.6667
all: HelloWorldBullet
clean:
rm HelloWorldBullet *~
40
30
20
10
50
100
150
200 250
Como vimos en la seccin 19.2.2, los motores de simulacin fsica utilizan varias
capas para detectar las colisiones entre los objetos del mundo. Bullet permite utilizar
algoritmos en una primera etapa (broadphase) que utilizan las cajas lmite de los objetos del mundo para obtener una lista de pares de cajas que pueden estar en colisin.
Esta lista es una lista exhaustiva de todas las cajas lmite que intersecan en alguno
de los ejes (aunque, en etapas posteriores lleguen a descartarse porque en realidad no
colisionan).
Muchos sistemas de simulacin fsica se basan en el Teorema de los Ejes Separados. Este teorema dice que si existe un eje sobre el que la proyeccin de dos formas
convexas no se solapan, entonces podemos asegurar que las dos formas no colisionan. Si no existe dicho eje y las dos formas son convexas, podemos asegurar que las
formas colisionan. Si las formas son cncavas, podra ocurrir que no colisionaran (dependiendo de la suerte que tengamos con la forma de los objetos). Este teorema se
puede visualizar fcilmente en 2 dimensiones, como se muestra en la Figura 19.23.a.
El mismo teorema puede aplicarse para cajas AABB. Adems, el hecho de que las
cajas AABB estn perfectamente alineadas con los ejes del sistema de referencia, hace
que el clculo de la proyeccin de estas cajas sea muy rpida (simplemente podemos
utilizar las coordenadas mnimas y mximas que denen las cajas).
La Figura 19.23 representa la aplicacin de este teorema sobre cajas AABB. En
el caso c) de dicha gura puede comprobarse que la proyeccin de las cajas se solapa
en todos los ejes, por lo que tendramos un caso potencial de colisin. En realidad,
como vemos en la gura las formas que contienen dichas cajas AABB no colisionan,
pero este caso deber ser resuelto por algoritmos de deteccin de colisin de menor
granularidad.
C19
50
[480]
a)
b)
Pr. B
Pr. A
Proyeccin
de B en X
c)
Proyeccin
de A en X
Figura 19.23: Aplicacin del Teorema de los Ejes Separados y uso con cajas AABB. a) La proyeccin
de las formas convexas de los objetos A y B estn separadas en el eje X, pero no en el eje Y. Como
podemos encontrar un eje sobre el que ambas proyecciones no intersecan, podemos asegurar que las formas
no colisionan. b) El mismo principio aplicados sobre cajas AABB denidas en objetos cncavos, que no
intersecan. c) La proyeccin de las cajas AABB se solapa en todos los ejes. Hay un posible caso de colisin
entre formas.
En la lnea 5 creamos un objeto que implementa un algoritmo de optimizacin
en una primera etapa broadphase. En posteriores etapas Bullet calcular las colisiones
exactas. Existen dos algoritmos bsicos que implementa Bullet para mejorar al aproximacin a ciegas de complejidad O(n2 ) que comprobara toda la lista de pares. Estos
algoritmos aaden nuevas parejas de cajas que en realidad no colisionan, aunque en
general mejoran el tiempo de ejecucin.
rbol AABB Dinmico. Este algoritmo est implementado en la clase btDbvtBroadphase. Se construye un rbol AABB de propsito general que se utiliza
tanto en la primera etapa de optimizacin broadphase como en la deteccin de
colisiones entre softbodies. Este tipo de arbol se adapta automticamente a las
dimensiones del mundo, y la insercin y eliminacin de objetos es ms rpido
que en SAP.
Barrido y Poda (SAP). La implementacin de Sweep and Prune de Bullet requiere que el tamao del mundo sea conocido de previamente. Este mtodo es
el que tiene mejor comportamiento en mundos dinmicos donde la mayora de
los objetos tienen poco movimiento. Se implementa en el conjunto de clases
AxisSweep (con versiones de diverso nivel de precisin).
Tras esta primera poda, hemos eliminado
gran cantidad de objetos que no colisionan. A continuacin, en las lneas 6-8 se crea un objeto de conguracin de la
colisin, que nos permitir adaptar los parmetros de los algoritmos utilizados en posteriores fases para comprobar la colisin. El btCollisionDispatcher es una clase que
permite aadir funciones de callback para ciertos tipos de eventos (como por ejemplo,
cuando los objetos se encuentren cerca).
El objeto solver (lnea 9 ) se encarga de que los objetos interacten adecuadamente, teniendo en cuenta la gravedad, las fuerzas, colisiones y restricciones. En este
ejemplo se ha utilizado la versin secuencial (que implementa el mtodo de Gauss
Seidel proyectado (PGS), para resolver problemas lineales), aunque existen versiones
que hacen uso de paralelismo empleando hilos.
En la lnea 10 se instancia el mundo. Este objeto nos permitir aadir los objetos
del mundo,
aplicar gravedad, y avanzar el paso de la simulacin. En concreto, en la
lnea 12 se establece una de las propiedades del mundo, la gravedad, asignando un
valor de 10m/s en el eje Y, por lo que se aplicar sobre ese eje la fuerza de gravedad.
Reutiliza!!
Es buena prctica reutilizar formas
de colisin. Si varios objetos de la
escena pueden compartir la misma
forma de colisin (por ejemplo, todos los enemigos pueden gestionarse con una misma esfera de un determinado radio), es buena prctica
compartir esa forma de colisin entre todos ellos.
[481]
Al nalizar, Bullet requiere que el usuario libere
la memoria que ha utilizado explcitamente. De esta forma, a partir de la lnea 42 se eliminan todos los elementos
que han sido creados a lo largo de la simulacin.
Hasta aqu hemos denido lo que puede ser el esqueleto bsico de una aplicacin
mnima de Bullet. Vamos a denir a continuacin los objetos que forman nuestra
escena y el bucle de simulacin.
Formas de Colisin
Como hemos comentado al inicio de la seccin, crearemos un objeto plano que
servir como suelo sobre el que dejaremos caer una esfera. Cada uno de estos cuerpos
necesita una forma de colisin, que internamente nicamente se utiliza para calcular
la colisin (no tiene propiedades de masa, inercia, etc...).
Las formas de colisin no tienen una posicin en el mundo; se adjuntan a los
cuerpos rgidos. La eleccin de la forma de colisin adecuada, adems de mejorar el
rendimiento de la simulacin, ayuda a conseguir una simulacin de calidad. Bullet
permite el uso de primitivas (que implementan algoritmos de deteccin de colisiones
muy optimizados) o mallas poligonales. Las primitivas soportadas por Bullet son:
btSphereShape. Esfera; la primitiva ms simple y rpida.
btBoxShape. La caja puede tener cualquier relacin de aspecto.
btCylinderShape. Cilindro con cualquier relacin de aspecto.
btCapsuleShape. Cpsula con cualquier relacin de aspecto.
btConeShape. Los conos se denen con el vrtice en el (0,0,0).
btCompoundShape. No es una primitiva bsica en s, sino que permite combinar formas de cualquier tipo (tanto primitivas como formas de colisin de tipo
malla que veremos a continuacin). Permite obtener formas compuestas, como
la que se estudi en la Figura 19.14.
Las formas de colisin de tipo malla soportadas son:
btConvexHull. Este es el tipo de forma de tipo malla ms rpido. Se dene como una nube de vrtices que forman la forma convexa ms pequea posible. El
nmero de vrtices debe ser pequeo para que la forma funcione adecuadamente. El nmero de vrtices puede reducirse empleando la utilidad proporcionada
por la clase btShapeHull. Existe una versin similar a este tipo llamado btConvexTriangleMeshShape, que est formado por caras triangulares, aunque es
deseable utilizar btConvexHull porque es mucho ms eciente.
btBvhTriangleMeshShape. Malla triangular esttica. Puede tener un nmero
considerable de polgonos, ya que utiliza una jerarqua interna para calcular la
colisin. Como la construccin de esta estructura de datos puede llevar tiempo,
se recomienda serializar el rbol para cargarlo rpidamente. Bullet incorpora
utilidades para la serializacin y carga del rbol BVH.
btHeighteldTerrainShape. Malla poligonal esttica optimizada descrita por
un mapa de alturas.
C19
[482]
En la lnea 16-17 creamos una forma de colisin de tipo plano, pasando como
parmetro el vector normal del plano (vector unitario en Y), y una distancia respecto
del origen. As, el plano de colisin queda denido por la ecuacin y = 1.
Una vez denidas las formas de colisin, las posicionaremos asocindolas a instancias de cuerpos rgidos. En la siguiente subseccin aadiremos los cuerpos rgidos
al mundo.
Cuerpos Rgidos
Para aadir cuerpos rgidos, necesitamos primero denir el concepto de MotionState en Bullet. Un MotionState es una abstraccin proporcionada por Bullet para actualizar la posicin de los objetos que sern dibujados en el game loop. Empleando
MotionStates, Bullet se encargar de actualizar los objetos que sern representados por
el motor grco. En la siguiente seccin estudiaremos cmo trabajar con MotionStates
en Ogre.
Gracias al uso de MotionStates, nicamente se actualiza la posicin de los objetos
que se han movido. Bullet se encarga adems de la interpolacin de movimientos,
aislando al programador de esta tarea. Cuando se consulte la posicin de un objeto,
por defecto se devolver la correspondiente al ltimo paso de simulacin calculado.
Sin embargo, cada vez que el motor grco necesite redibujar la escena, Bullet se
encargar de devolver la transformacin interpolada.
Los MotionStates deben utilizarse en dos situaciones:
1. Cuando se crea un cuerpo. Bullet determina la posicin inicial del cuerpo en el
momento de su creacin, y requiere una llamada al MotionState.
2. Cuando se quiera actualizar la posicin del objeto.
MotionState propio
Para implementar nuestro propio
MotionState basta con heredar
de
btMotionState
y sobreescribir los mtodos
getWorldTransform
y
setWorldTransform.
[483]
El primer parmetro es la masa del objeto. Estableciendo una masa igual a cero
(primer parmetro), se crea un objeto esttico (equivale a establecer una masa innita,
de modo que el objeto no se puede mover). El ltimo parmetro es la inercia del suelo
(que se establece igualmente a 0, por ser un objeto esttico).
En la lnea 24 creamos el objeto rgido a partir de la informacin
almacenada en
28
).
En las lneas 29-31 se establecen las propieades del cuerpo; una masa de 1Kg y
se llama a un mtodo de btCollisionShape que nos calcula la inercia de una esfera a
partir de su masa.
Bucle Principal
Tiempos!
Cuidado, ya que las funciones de
clculo de tiempo habitualmente
devuelven los resultados en milisegundos. Bullet trabaja en segundos,
por lo que sta es una fuente habitual de errores.
No olvides...
Cuando cambies el valor de los
tiempos de simulacin, recuerda
calcular el nmero de subpasos de
simulacin para que la ecuacin siga siendo correcta.
Decrementando el tamao de cada paso de simulacin se est aumentado la resolucin de la simulacin fsica. De este modo, si en el juego hay objetos que atraviesan objetos (como paredes), es posible decrementar el xedTimeStep para aumentar
la resolucin. Obviamente, cuando se aumenta la resolucin al doble, se necesitar
aumentar el nmero de maxSubSteps al doble, lo que requerir aproximadamente el
doble de tiempo de CPU para el mismo tiempo de simulacin fsica.
C19
Para nalizar, el bucle principal se ejecuta en las lneas 36-41 . El bucle se ejecuta
300 veces, llamando al paso de simulacin con un intervalo de 60hz. En cada paso de
la simulacin se imprime la altura de la esfera sobre el suelo.
[484]
Cuando se especica un valor de maxSubSteps > 1, Bullet interpolar el movimiento (y evitar al programador tener que realizar los clculos). Si maxSubSteps ==
1, no realizar interpolacin.
19.6.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Ogre.h>
#include <btBulletDynamicsCommon.h>
class MyMotionState : public btMotionState {
protected:
Ogre::SceneNode* _visibleobj;
btTransform _pos;
public:
MyMotionState(const btTransform &initialpos,
Ogre::SceneNode* node);
virtual ~MyMotionState();
void setNode(Ogre::SceneNode* node);
virtual void getWorldTransform(btTransform &worldTr) const;
virtual void setWorldTransform(const btTransform &worldTr);
};
btTransform
Las transformaciones de cuerpo rgido estn formadas nicamente
por traslaciones y rotaciones (sin
escalado). As, esta clase utiliza
internamente un btVector3 para la
traslacin y una matriz 3x3 para almacenar la rotacin.
[485]
setPosition (lneas 17 y 19 ).
La llamada a setWorldTransform puede retornar en la lnea 15 si no se ha establecido nodo en el constructor. Se habilita un mtodo especco para establecer el nodo
ms adelante. Esto es interesante si se quieren aadir objetos a la simulacin que no
tengan representacin grca.
Listado 19.4: MyMotionState.cpp
#include "MyMotionState.h"
MyMotionState::MyMotionState(const btTransform &initialpos,
Ogre::SceneNode *node) {
_visibleobj = node; _pos = initialpos;
}
void MyMotionState::setNode(Ogre::SceneNode *node)
{ _visibleobj = node; }
void MyMotionState::getWorldTransform (btTransform &worldTr) const
{ worldTr = _pos; }
void MyMotionState::setWorldTransform(const btTransform &worldTr){
if(NULL == _visibleobj) return;
// Si no hay nodo, return
btQuaternion rot = worldTr.getRotation();
_visibleobj->setOrientation(rot.w(), rot.x(), rot.y(), rot.z());
btVector3 pos = worldTr.getOrigin();
_visibleobj->setPosition(pos.x(), pos.y(), pos.z());
}
Una vez creada la clase que utilizaremos para denir el MotionState, la utilizaremos en el Hola Mundo construido en la seccin anterior para representar la simulacin con el plano y la esfera. El resultado que tendremos se muestra en la Figura 19.25.
Para la construccin del ejemplo emplearemos como esqueleto base el FrameListener
del Mdulo 2 del curso.
Listado 19.5: MyFrameListener.cpp
Figura 19.25: Fragmento del resultado de integracin del Hola Mundo de Bullet en Ogre, empleando
la clase de MyMotionState denida
anteriormente.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "MyFrameListener.h"
#include "MyMotionState.h"
MyFrameListener::MyFrameListener(RenderWindow* win,
Camera* cam, OverlayManager *om, SceneManager *sm) {
// .... Omitida parte de la inicializacion
_broadphase = new btDbvtBroadphase();
_collisionConf = new btDefaultCollisionConfiguration();
_dispatcher = new btCollisionDispatcher(_collisionConf);
_solver = new btSequentialImpulseConstraintSolver;
_world = new btDiscreteDynamicsWorld(_dispatcher,_broadphase,
_solver,_collisionConf);
_world->setGravity(btVector3(0,-10,0));
CreateInitialWorld();
}
MyFrameListener::~MyFrameListener() {
_world->removeRigidBody(_fallRigidBody);
delete _fallRigidBody->getMotionState();
delete _fallRigidBody;
// ... Omitida la eliminacion de los objetos
}
C19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[486]
24 void MyFrameListener::CreateInitialWorld() {
25
// Creacion de la entidad y del SceneNode ----------------------26
Plane plane1(Vector3::Vector3(0,1,0), 0);
27
MeshManager::getSingleton().createPlane("p1",
28
ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane1,
29
200, 200, 1, 1, true, 1, 20, 20, Vector3::UNIT_Z);
30
SceneNode* node = _sceneManager->createSceneNode("ground");
31
Entity* groundEnt = _sceneManager->createEntity("planeEnt","p1");
32
groundEnt->setMaterialName("Ground");
33
node->attachObject(groundEnt);
34
_sceneManager->getRootSceneNode()->addChild(node);
35
36
// Creamos las formas de colision ------------------------------37
_groundShape = new btStaticPlaneShape(btVector3(0,1,0),1);
38
_fallShape = new btSphereShape(1);
39
40
// Creamos el plano --------------------------------------------41
MyMotionState* groundMotionState = new MyMotionState(
42
btTransform(btQuaternion(0,0,0,1),btVector3(0,-1,0)), node);
43
btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI
44
(0,groundMotionState,_groundShape,btVector3(0,0,0));
45
_groundRigidBody = new btRigidBody(groundRigidBodyCI);
46
_world->addRigidBody(_groundRigidBody);
47
48
// Creamos la esfera -------------------------------------------49
Entity *entity2= _sceneManager->createEntity("ball","ball.mesh");
50
SceneNode *node2= _sceneManager->getRootSceneNode()->
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
createChildSceneNode();
node2->attachObject(entity2);
MyMotionState* fallMotionState = new MyMotionState(
btTransform(btQuaternion(0,0,0,1),btVector3(0,50,0)), node2);
btScalar mass = 1;
btVector3 fallInertia(0,0,0);
_fallShape->calculateLocalInertia(mass,fallInertia);
btRigidBody::btRigidBodyConstructionInfo fallRigidBodyCI(
mass,fallMotionState,_fallShape,fallInertia);
_fallRigidBody = new btRigidBody(fallRigidBodyCI);
_world->addRigidBody(_fallRigidBody);
// Actualizar fisica
_keyboard->capture();
if (_keyboard->isKeyDown(OIS::KC_ESCAPE)) return false;
btVector3 impulse;
if (_keyboard->isKeyDown(OIS::KC_I)) impulse=btVector3(0,0,-.1);
if (_keyboard->isKeyDown(OIS::KC_J)) impulse=btVector3(-.1,0,0);
if (_keyboard->isKeyDown(OIS::KC_K)) impulse=btVector3(0,0,.1);
if (_keyboard->isKeyDown(OIS::KC_L)) impulse=btVector3(.1,0,0);
_fallRigidBody->applyCentralImpulse(impulse);
Variables miembro
Mantener los objetos como variables miembro de la clase no deja
de ser una mala decisin de diseo.
En la seccin 19.7 veremos cmo se
gestionan listas dinmicas con los
objetos y las formas de colisin.
[487]
En la implementacin del FrameListener es necesario mantener como variables
miembro el conjunto de objetos necesarios
en la simulacin de Bullet. As, la im
plementacin del constructor (lneas 4-15
los objetos necesarios para crear
dene
el mundo de simulacin de Bullet
11-12 ). Estos objetos sern liberados en el
(lnea
destructor de la clase (ver lneas 17-22 ). De igual modo, los dos cuerpos rgidos que
intervienen en la simulacin y sus formas asociadas son variables miembro de la clase.
El mtodo CreateInitialWorld (denido en las lneas 24-60 ) se realiza como ltimo paso en el constructor. En este mtodo se aaden a la escena de Ogre y al mundo
de Bullet los elementos que intervendrn en la simulacin (en este caso la esfera y el
plano).
La creacin de las entidades y los nodos para el plano y la esfera (lneas 26-34 y
49-51 respectivamente) ya han sido estudiadas
en elMdulo 2 del curso. La creacin
de las formas para el plano y la esfera (lneas 37-38 ) fueron descritas en el cdigo
de
la seccin anterior. Cabe destacar que la malla exportada en ball.mesh (lnea 49
)
debe tener un radio de 1 unidad, para que la forma de colisin denida en la lnea 38
se adapte bien a su representacin grca.
Cada objeto tendr asociado un MotionState de la clase denida anteriormente,
que recibir la rotacin y traslacin inicial, y el puntero al nodoque guarda la entidad
a representar. En el caso del plano, se dene en las lneas 41-42 , y la esfera en 52-53 .
Por ltimo, tendremos que aadir cdigo especco en los mtodos de retrollamada de actualizacin
del
anterior se muestra el cdigo de frameS
frame. En el listado
Para nalizar, se muestran los ags del Makele necesarios para integrar Bullet en
los ejemplos anteriores.
Listado 19.6: Fragmento de Makele
1 # Flags de compilacion ----------------------------2 CXXFLAGS := -I $(DIRHEA) -Wall pkg-config --cflags OGRE pkg-
config
--cflags bullet
3
4 # Flags del linker ------------------------------5 LDFLAGS := pkg-config --libs-only-L OGRE pkg-config --libs-only-
bullet
19.7.
El desarrollo de un wrapper completo del motor Bullet puede ser una tarea costosa.
Afortunadamente existen algunas alternativas que facilitan la integracin del motor en
Ogre, como el proyecto OgreBullet. OgreBullet se distribuye bajo una licencia MIT
libre, y es multiplataforma.
C19
[488]
En el siguiente ejemplo crearemos una escena donde se aadirn de forma dinmica cuerpos rgidos. Adems de un puntero al DynamicsWorld (variable miembro
_world), el FrameListener mantiene un puntero a un objeto _debugDrawer (lnea 7 ),
que nos permite representar cierta informacin visual que facilita el depurado de la
aplicacin.
En este primer ejemplo se activa el dibujado de las formas de colisin
(lnea 8 ), tal y como se muestra en la Figura 19.26.
Este objeto permite aadir otros elementos que faciliten la depuracin, como lneas, puntos de contacto, cajas AABBs, etc. Los mtodos relativos a la representacin
de texto 3D en modo depuracin estn previstos pero an no se encuentran desarrollados. Este objeto
de depuracin debe ser aadido igualmente al grafo de escena de
simulacin. En las lneas 14-15 se crea una caja AABB descrita por los vrtices de
sus esquinas que dene el volumen en el que se realizar la simulacin fsica.Este
lmite, junto con el vector de gravedad, permitirn crear el mundo (lneas 18-19 ).
Listado 19.7: Constructor
1 MyFrameListener::MyFrameListener(RenderWindow* win,
2
Camera* cam, OverlayManager *om, SceneManager *sm) {
3
_numEntities = 0;
// Numero de Formas instanciadas
4
_timeLastObject = 0; // Tiempo desde ultimo objeto anadido
5
6
// Creacion del modulo de debug visual de Bullet --------------7
_debugDrawer = new OgreBulletCollisions::DebugDrawer();
8
_debugDrawer->setDrawWireframe(true);
9
SceneNode *node = _sceneManager->getRootSceneNode()->
10
createChildSceneNode("debugNode", Vector3::ZERO);
11
node->attachObject(static_cast<SimpleRenderable*>(_debugDrawer));
12
// Creacion del mundo (definicion de los limites y la gravedad)
13
AxisAlignedBox worldBounds = AxisAlignedBox
14
(Vector3(-10000,-10000,-10000), Vector3(10000,10000,10000));
15
Vector3 gravity = Vector3(0, -9.8, 0);
16
_world = new OgreBulletDynamics::DynamicsWorld(_sceneManager,
17
worldBounds, gravity);
18
_world->setDebugDrawer (_debugDrawer);
19
_world->setShowDebugShapes (true); // Muestra formas debug
20
CreateInitialWorld();
// Inicializa el mundo
21 }
4 Foros
OgreBullet: http://www.ogre3d.org/addonforums/viewforum.php?f=12
[489]
El FrameListener mantiene dos colas de doble n (deque) de punteros a los RigidBody (_bodies) y a las CollisionShape (_shapes), que facilitan la insercin y borrado
de elementos de un modo ms rpido que los vectores. As, cuando se aadan objetos
de forma dinmica a la escena, ser necesario aadirlos a estas estructuras para su
posterior liberacin en el destructor de la clase. El siguiente listado muestra la implementacin del destructor del FrameListener.
bsicos; por un
lado
la
CollisionShape
(lneas
14-16
(lneas 17-18 ).
La asociacin del nodo de dibujado con el cuerpo rgido se establece en la misma
llamada en la que se asocia la forma de colisin al RigidBody. En la clase OgreBulletDynamics::RigidBody existen dos mtodos que permiten asociar una forma de
colisin a un RigidBody; setShape y setStaticShape. La segunda cuenta con varias
versiones; una de ellas
requiere especicar el SceneNode, y se corresponde con la
no
utilizada en la lnea 21 para aadir la forma de colisin al plano.
Listado 19.9: CreateInitialWorld
1 void MyFrameListener::CreateInitialWorld() {
2
// Creacion de la entidad y del SceneNode ----------------------3
Plane plane1(Vector3::Vector3(0,1,0), 0);
4
MeshManager::getSingleton().createPlane("p1",
5
ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane1,
6
200, 200, 1, 1, true, 1, 20, 20, Vector3::UNIT_Z);
7
SceneNode* node= _sceneManager->createSceneNode("ground");
8
Entity* groundEnt= _sceneManager->createEntity("planeEnt", "p1");
9
groundEnt->setMaterialName("Ground");
10
node->attachObject(groundEnt);
11
_sceneManager->getRootSceneNode()->addChild(node);
12
13
// Creamos forma de colision para el plano ---------------------14
OgreBulletCollisions::CollisionShape *Shape;
15
Shape = new OgreBulletCollisions::StaticPlaneCollisionShape
16
(Ogre::Vector3(0,1,0), 0);
// Vector normal y distancia
17
OgreBulletDynamics::RigidBody *rigidBodyPlane = new
18
OgreBulletDynamics::RigidBody("rigidBodyPlane", _world);
19
20
// Creamos la forma estatica (forma, Restitucion, Friccion) ----
C19
1 MyFrameListener::~MyFrameListener() {
2
// Eliminar cuerpos rigidos -----------------------------------3
std::deque <OgreBulletDynamics::RigidBody *>::iterator
4
itBody = _bodies.begin();
5
while (_bodies.end() != itBody) {
6
delete *itBody; ++itBody;
7
}
8
// Eliminar formas de colision --------------------------------9
std::deque<OgreBulletCollisions::CollisionShape *>::iterator
10
itShape = _shapes.begin();
11
while (_shapes.end() != itShape) {
12
delete *itShape; ++itShape;
13
}
14
_bodies.clear(); _shapes.clear();
15
// Eliminar mundo dinamico y debugDrawer ----------------------16
delete _world->getDebugDrawer();
_world->setDebugDrawer(0);
17
delete _world;
18 }
[490]
21
rigidBodyPlane->setStaticShape(Shape, 0.1, 0.8);
22
23
// Anadimos los objetos Shape y RigidBody ---------------------24
_shapes.push_back(Shape);
_bodies.push_back(rigidBodyPlane);
25 }
1 void MyFrameListener::AddDynamicObject() {
2
_timeLastObject = 0.25;
// Segundos para anadir uno nuevo...
3
4
Vector3 size = Vector3::ZERO;
// Tamano y posicion inicial
5
Vector3 position = (_camera->getDerivedPosition()
6
+ _camera->getDerivedDirection().normalisedCopy() * 10);
7
8
// Creamos la entidad y el nodo de la escena ------------------9
Entity *entity = _sceneManager->createEntity("Box" +
10
StringConverter::toString(_numEntities), "cube.mesh");
11
entity->setMaterialName("cube");
12
SceneNode *node = _sceneManager->getRootSceneNode()->
13
createChildSceneNode();
14
node->attachObject(entity);
15
16
// Obtenemos la bounding box de la entidad creada -------------17
AxisAlignedBox boundingB = entity->getBoundingBox();
18
size = boundingB.getSize();
19
size /= 2.0f;
// Tamano en Bullet desde el centro (la mitad)
20
OgreBulletCollisions::BoxCollisionShape *boxShape = new
21
OgreBulletCollisions::BoxCollisionShape(size);
22
OgreBulletDynamics::RigidBody *rigidBox = new
23
OgreBulletDynamics::RigidBody("rigidBox" +
24
StringConverter::toString(_numEntities), _world);
25
rigidBox->setShape(node, boxShape,
26
/* Restitucion, Friccion, Masa */ 0.6, 0.6, 5.0,
27
/* Pos. y Orient. */ position , Quaternion::IDENTITY);
28
rigidBox->setLinearVelocity(
29
_camera->getDerivedDirection().normalisedCopy() * 7.0);
30
_numEntities++;
31
// Anadimos los objetos a las deques --------------------------32
_shapes.push_back(boxShape);
_bodies.push_back(rigidBox);
stepSimulation
La llamada a stepSimulation en
OgreBullet acepta dos parmetros
opcionales, el nmero de subpasos
de simulacin (por defecto a 1), y el
xedTimeStep (por defecto 1/60).
19.8. RayQueries
[491]
33 }
En el listado
anterior,
el nodo asociado a cada caja se aade en la llamada a setSha
19.8.
RayQueries
Al inicio del captulo estudiamos algunos tipos de preguntas que podan realizarse
al motor de simulacin fsica. Uno de ellos eran los RayQueries que permitan obtener
las formas de colisin que intersecaban con un determinado rayo.
Recordemos que los objetos de simulacin fsica no son conocidos
por Ogre. Aunque en el mdulo 2
del curso estudiamos los RayQueries en Ogre, es necesario realizar la
pregunta en Bullet para obtener las
referencias a los RigidBody.
trimeshConverter = NULL;
C19
Ogre? Bullet?
[492]
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 }
El objeto de la clase StaticMeshToShapeConverter recibe como parmetro una Entity de Ogre en el constructor. Esta entidad puede ser convertida a multitud de formas
de colisin. En el momento de la creacin, la clase reduce el nmero de vrtices de la
forma de colisin.
Cuando se pincha con el botn derecho o izquierdo del ratn sobre algn objeto
de
la
simulacin, se aplicar un impulso con diferente fuerza (denida en F , ver lnea
18 del siguiente cdigo). El mtodo pickBody se encarga de obtener el primer cuerpo
que colisiona con el rayo denido por la posicin de la cmara y el puntero del ratn.
Este mtodo devuelve igualmente en los dos primeros parmetros el punto de colisin
en el objeto y el rayo utilizado para construir el RayQuery.
El mtodo pickBody primero obtiene el rayo utilizando la funcionalidad
de Ogre,
float y) {
2
r = _camera->getCameraToViewportRay (x, y);
3
CollisionClosestRayResultCallback cQuery =
4
CollisionClosestRayResultCallback (r, _world, 10000);
5
_world->launchRay(cQuery);
6
if (cQuery.doesCollide()) {
7
RigidBody* body = static_cast <RigidBody *>
8
(cQuery.getCollidedObject());
9
p = cQuery.getCollisionPoint();
10
return body;
11
}
12
return NULL;
13 }
14
15 bool MyFrameListener::frameStarted(const Ogre::FrameEvent& evt) {
16
// Omitido codigo anterior del metodo --------------------------17
if (mbleft || mbright) { // Con botones del raton, impulso -----18
float F = 10; if (mbright) F = 100;
19
RigidBody* body; Vector3 p; Ray r;
20
float x = posx/float(_win->getWidth());
// Pos x normalizada
21
float y = posy/float(_win->getHeight()); // Pos y normalizada
22
body = pickBody (p, r, x, y);
Conversor a Shape
Adems de la llamada a createConvex, el conversor estudiado en el cdigo anterior puede generar otras
formas de colisin con createSphere, createBox, createTrimesh, createCylinder y createConvexDecomposition entre otras.
19.9. TriangleMeshCollisionShape
[493]
23
24
if (body) {
25
if (!body->isStaticObject()) {
26
body->enableActiveState ();
27
Vector3 relPos(p - body->getCenterOfMassPosition());
28
Vector3 impulse (r.getDirection ());
29
body->applyImpulse (impulse * F, relPos);
30
}
31
}
32
}
33
// Omitido resto de codigo del metodo --------------------------34 }
Para nalizar, si hubo colisin con algn cuerpo que no sea esttico (lneas 24-25 ),
se aplicar un impulso. La llamada a enableActiveState permite activar un cuerpo. Por
defecto, Bullet automticamente desactiva objetos dinmicos cuando la velocidad es
menor que un determinado umbral.
Impulso
El impulso puede denirse como
F dt = (dp/dt)dt, siendo p el
momento.
Los cuerpos desactivados en realidad estn se encuentran en un estado de dormidos, y no consumen tiempo de ejecucin salvo por la etapa de deteccin de colisin
broadphase. Esta etapa automticamente despierta a los objetos que estuvieran dormidos si se encuentra colisin con otros elementos de la escena.
En las lneas 27-29 se aplica un impulso sobre el objeto en la direccin del rayo
que se calcul desde la cmara, con una fuerza proporcional a F . El impulso es una
fuerza que acta en un cuerpo en un determinado intervalo de tiempo. El impulso
implica un cambio en el momento, siendo la Fuerza denida como el cambio en el
momento. As, el impulso aplicado sobre un objeto puede ser denido como la integral
de la fuerza con respecto del tiempo.
19.9.
TriangleMeshCollisionShape
En este ejemplo se cargan dos objetos como mallas triangulares estticas. El resultado de la ejecucin puede verse en la Figura 19.28. Al igual que en el ejemplo
anterior, se utiliza la funcionalidad proporcionada por el conversor de mallas, pero
generando una TriangleMeshCollisionShape (lnea 11-12 ).
Listado 19.14: Static Mesh
1 void MyFrameListener::CreateInitialWorld() {
2
// Creacion del track -----------------------------------------3
Entity *entity = _sceneManager->createEntity("track.mesh");
4
SceneNode *node = _sceneManager->createSceneNode("track");
5
node->attachObject(entity);
6
7
_sceneManager->getRootSceneNode()->addChild(node);
8
OgreBulletCollisions::StaticMeshToShapeConverter *
9
10
11
12
13
14
15
trimeshConverter = new
OgreBulletCollisions::StaticMeshToShapeConverter(entity);
OgreBulletCollisions::TriangleMeshCollisionShape *trackTrimesh =
trimeshConverter->createTrimesh();
OgreBulletDynamics::RigidBody *rigidTrack = new
OgreBulletDynamics::RigidBody("track", _world);
C19
[494]
Figura 19.29: Conguracin de la malla esttica utilizada en el ejemplo. Es importante aplicar la escala y
rotacin a los objetos antes de su exportacin, as como las dimensiones del objeto ball para aplicar los
mismos lmites a la collision shape.
16
rigidTrack->setShape(node, trackTrimesh, 0.8, 0.95, 0,
17
Vector3::ZERO, Quaternion::IDENTITY);
18
19
delete trimeshConverter;
20
// (Omitido) Creacion del sumidero de forma similar -----------21 }
19.10.
Deteccin de colisiones
Una de las formas ms sencillas de detectar colisiones entre objetos del mundo es
iterar sobre los colectores de contactos (contact manifold). Los contact manifold son
en realidad caches que contienen los puntos de contacto entre parejas de objetos de
colisin. El siguiente listado muestra una forma de iterar sobre los pares de objetos en
el mundo dinmico.
Listado 19.15: DetectCollisionDrain.
1 void MyFrameListener::DetectCollisionDrain() {
2
btCollisionWorld *bulletWorld=_world->getBulletCollisionWorld();
3
int numManifolds=bulletWorld->getDispatcher()->getNumManifolds();
4
5
for (int i=0;i<numManifolds;i++) {
6
btPersistentManifold* contactManifold =
7
bulletWorld->getDispatcher()->getManifoldByIndexInternal(i);
8
btCollisionObject* obA =
9
static_cast<btCollisionObject*>(contactManifold->getBody0());
10
btCollisionObject* obB =
11
static_cast<btCollisionObject*>(contactManifold->getBody1());
12
13
Ogre::SceneNode* drain = _sceneManager->getSceneNode("drain");
14
[495]
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
}
35 }
En la lnea 2 se obtiene el puntero directamente a la clase btCollisionWorld, que
se encuentra oculta en la implementacin de OgreBullet. Con este puntero se acceder
directamente a la funcionalidad de Bullet sin emplear la clase de recubrimieneto de
OgreBullet. La clase btCollisionWorld sirve a la vez como interfaz y como contenedor
de las funcionalidades relativas a la deteccin de colisiones.
Mediante la llamada a getDispatcher (lnea 3 ) se obtiene un puntero a la clase
btDispather, que se utiliza en la fase de colisin broadphase para la gestin de pares
de colisin. Esta clase nos permite obtener
el nmero de colectores que hay activos en
la lnea 6-7 se obtiene un puntero a un objeto de la clase btPersistentManifold. Esta
clase es una implementacin de una cach persistente mientras los objetos colisionen
en la etapa de colisin broadphase.
La cache de colisin mantiene punteros a los dos objetos que estn colisionando.
Estos objetos pueden obtenerse mediante la llamada a mtodos get (lneas 8-11 ).
La ltima parte del cdigo (lneas 20-32 ) comprueba si alguno de los dos objetos
de la colisin son el sumidero. En ese caso, se obtiene el puntero al otro objeto (que
se corresponder con un objeto de tipo esfera creado dinmicamente), y se elimina de
la escena. As, los objetos en esta segunda versin del ejemplo no llegan a aadirse en
la caja de la parte inferior del circuito.
C19
OgreBullet...
OgreBulletCollisions::Object *obDrain =
_world->findObject(drain);
OgreBulletCollisions::Object *obOB_A = _world->findObject(obA);
OgreBulletCollisions::Object *obOB_B = _world->findObject(obB);
[496]
19.11.
Restriccin de Vehculo
[497]
OgreBulletDynamics::WheeledRigidBody
OgreBulletDynamics::VehicleTuning
OgreBulletDynamics::VehicleRayCaster
OgreBulletDynamics::RaycastVehicle
Ogre::Entity
*mChassis;
Ogre::Entity
*mWheels[4];
Ogre::SceneNode *mWheelNodes[4];
float mSteering;
*mCarChassis;
*mTuning;
*mVehicleRayCaster;
*mVehicle;
En el anterior archivo de cabecera se denen ciertas variables miembro de la clase que se utilizarn en la denicin del vehculo. mCarChassis es un puntero a una
clase que ofrece OgreBullet para la construccin de vehculos con ruedas. La clase
VehicleTuning es una clase de cobertura sobre la clase btVehicleTuning de Bullet que
permite especicar ciertas propiedades del vehculo (como la compresin, suspensin,
deslizamiento, etc).
VehicleRayCaster es una clase que ofrece un interfaz entre la simulacin del vehculo y el RayCasting (usado para localizar el punto de contacto entre el vehculo y el
suelo). La clase RaycastVehicle es
una
clase de cobertura sobre la clase base de Bullet btRaycastVehicle. Las lneas 5-7 denen los nodos y entidades necesarias
para el
chasis y las ruedas del vehculo. Finalmente, la varible Steering de la lnea 8 dene
la direccin del vehculo.
C19
A continuacin estudiaremos algunos fragmentos de cdigo empleados en el siguiente ejemplo para la construccin del vehculo. Queda como ejercicio propuesto
aadir obstculos y elementos de interaccin en la escena empleando mallas triangulares estticas.
[498]
7
8 SceneNode *chassisnode = node->createChildSceneNode();
9 chassisnode->attachObject (mChassis);
10 chassisnode->setPosition (chassisShift);
El chasis tendr asociada una forma de colisin de tipo caja (lnea 1 ). Esta
caja
formar parte de una forma de colisin compuesta, que se dene en la lnea 2 , y a la
que se aade la caja anterior desplazada segn el vector chassisShift (lnea 3 ).
En la lnea 4 se dene el cuerpo rgido
del vehculo,
al que se asocia la forma de
colisin creada anteriormente (lnea 6 ). En la lnea 9 se establecen los valores
de
suspensin del vehculo, y se evita que el vehculo pueda desactivarse (lnea 8 ), de
modo que el objeto no se dormir incluso si se detiene durante un tiempo continuado.
Listado 19.18: Fragmento de CreateInitialWorld (II).
1 BoxCollisionShape* chassisShape = new BoxCollisionShape(Ogre::
Vector3(1.f,0.75f,2.1f));
CompoundCollisionShape* compound = new CompoundCollisionShape();
compound->addChildShape(chassisShape, chassisShift);
mCarChassis = new WheeledRigidBody("carChassis", _world);
Vector3 CarPosition = Vector3(0, 0, -15);
mCarChassis->setShape (node, compound, 0.6, 0.6, 800, CarPosition,
Quaternion::IDENTITY);
7 mCarChassis->setDamping(0.2, 0.2);
8 mCarChassis->disableDeactivation();
2
3
4
5
6
En el siguiente fragmento
se comienza deniendo algunos parmetros de tuning
El bucle de las lneas 10-16 construye los nodos de las ruedas, cargando 4 instancias de la malla wheel.mesh.
[499]
El ejemplo desarrollado en esta seccin trabaja nicamente con las dos ruedas
delanteras (ndices 0 y 1) como ruedas motrices. Adems, ambas ruedas giran
de forma simtrica segn la variable de direccin mSteering. Queda propuesto
como ejercicio modicar el cdigo de esta seccin para que la direccin se
pueda realizar igualmente con las ruedas traseras, as como incorporar otras
opciones de motricidad (ver Listado de FrameStarted).
mVehicleRayCaster);
mVehicle->setCoordinateSystem(0, 1, 2);
Ogre::Vector3 wheelDirectionCS0(0,-1,0);
Ogre::Vector3 wheelAxleCS(-1,0,0);
for (size_t i = 0; i < 4; i++) {
mWheels[i] = _sceneManager->createEntity("wheel"+i,"wheel.mesh");
mWheels[i]->setCastShadows(true);
15
16 }
mWheelNodes[i] = _sceneManager->getRootSceneNode()->
createChildSceneNode();
mWheelNodes[i]->attachObject (mWheels[i]);
connectionHeight, 2-gWheelRadius);
2
3 mVehicle->addWheel(mWheelNodes[0], connectionPointCS0,
C19
4
5
6
7
8
9
10
11
12
13
14
[500]
Figura 19.32: La gestin del determinismo puede ser un aspecto crtico en muchos videojuegos. El error
de determinismo rpidamente se propaga haciendo que la simulacin obtenga diferentes resultados.
8
9
10
11
12
13
14
15
16
17
18 }
if (_keyboard->isKeyDown(OIS::KC_LEFT)) {
if (mSteering < 0.8) mSteering+=0.01;
mVehicle->setSteeringValue (mSteering, 0);
mVehicle->setSteeringValue (mSteering, 1);
}
// Omitido el resto del codigo...
19.12.
Determinismo
19.12. Determinismo
[501]
Una posible manera de realizar la simulacin sera la siguiente: el motor de simulacin fsica se ejecuta por adelantado en intervalos de tiempo discretos dt, de modo
que se mantengan los incrementos del motor grco con un intervalo adecuado. Por
ejemplo, si queremos tener 50fps y la simulacin fsica se ejecuta a 100fps, entonces
tendramos que ejecutar dos veces la simulacin fsica por cada despliegue grco.
Esto es correcto con esos clculos sencillos, pero qu ocurre si queremos dibujar
a 200fps?. En ese caso tendramso que ejecutar la mitad de veces el simulador fsico,
pero no podemos calcular por adelantado un valos de dt. Adems, podra ocurrir que
no existiera un mltiplo cmodo para sincronizar el motor de simulacin fsica y el
motor de despliegue grco.
La forma de resolver el problema pasa por cambiar el modo de pensar en l. Podemos pensar que el motor de render produce tiempo, y el motor de simulacin fsica
tiene que consumirlo en bloques discretos de un tamao determinado.
Puede ayudar pensar que el motor de render produce chunks de tiempo discreto, mientras que el motor de simulacin fsica los consume.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
C19
A continuacin se muestra un sencillo game loop que puede emplearse para conseguir determinismo de una forma sencilla.
[502]
Como se indica en las lneas 6-7 , inicialmente se resetean los temporizadores.
El pseudocdigo del bucle principal del juego se resume en las lneas 9-21 . Tras
representar un
se obtiene el tiempo transcurrido desde la ltima simulacin
frame,
fsica (lnea 11 ), y se avanza un paso de simulacin en segundos (como la llamada al
sistema lo obtiene en milisegundos y Bullet lo requiere en segundos, hay que dividir
por 1000).
19.13.
19.14.
Serializacin
btDefaultSerializer*
serializer = new btDefaultSerializer();
dynamicsWorld->serialize(serializer);
19.14. Serializacin
[503]
En la posterior carga de los archivos .bullet, se debe utilizar la cabecera de BulletWorldImporter, creando un objeto de esa clase. El constructor de esa clase requiere
que se le especique el mundo dinmico sobre el que crear los objetos que fueron
serializados. El uso del importador puede resumirse en el siguiente fragmento de cdigo:
Listado 19.24: Importacin de datos serializados.
C19
1 #include "btBulletWorldImporter.h"
2 btBulletWorldImporter* f = new btBulletWorldImporter(_dynWorld);
3 f->loadFile("testFile.bullet");
Tcnicas Avanzadas
El objetivo de este mdulo lado Tcnicas Avanzadas dentro
del Curso de Experto en Desarrollo de Videojuegos, es profundizar
es aspectos de desarrollo ms avanzados que complementen el resto
de contenidos de dicho curso y permitan explorar soluciones ms
eficientes en el contexto del desarrollo de videojuegos. En este
mdulo se introducen aspectos bsicos de jugabilidad y se describen
algunas metodologas de desarrollo de videojuegos. As mismo,
tambin se estudian los fundamentos bsicos de la validacin y
pruebas en este proceso de desarrollo. No obstante, uno de los
componentes ms importantes del presente mdulo est relacionado
con aspectos avanzados del lenguaje de programacin C++, como
por ejemplo el estudio en profundidad de la biblioteca STL, y las
optimizaciones. Finalmente, el presente mdulo se complementa con
aspectos de representacin avanzada, como los filtros de partculas o
la programacin de shaders, y con un estudio en detalle de tcnicas
de optimizacin para escenarios interiores y exteriores. Por otra
parte, se realiza un estudio de la plataforma de desarrollo de
videojuegos Unity, especialmente ideada para el desarrollo de juegos
en plataformas mviles.
Captulo
20
Aspectos de Jugabilidad y
Metodologas de Desarrollo
Miguel ngel Redondo Duque
Jos Luis Gonzlez
n este captulo se introducen aspectos bsicos relativos al concepto de jugabilidad, como por ejemplo aquellos vinculados a su caractarizacin en el mbito
del desarrollo de videojuegos o las facetas ms relevantes de los mismos, haciendo especial hincapi en la parte relativa a su calidad.
Por otra parte, en este captulo tambin se discuten los fundamentos de las metodologas de desarrollo para videojuegos, estableciendo las principales fases y las
actividades desarrolladas en ellas.
20.1.
20.1.1.
Introduccin
En el desarrollo de sistemas interactivos es fundamental la participacin del usuario. Por ello, se plantean los denominados mtodos de Diseo Centrado en el Usuario
que se aplican, al menos, al desarrollo software que soporta directamente la interaccin con el usuario. En otras palabras, es fundamental contar con su participacin para
que tengamos la garanta de que se le va a proporcionar buenas experiencias de uso.
El software para videojuegos se puede considerar como un caso particular de sistema
interactivo por lo que requiere de un planteamiento similar en este sentido, aunque
en este mbito, los trminos y conceptos que se emplean para este propsito, varan
ligeramente.
507
[508]
20.1.2.
Caracterizacin de la Jugabilidad
[509]
atributos o propiedades que lo caracterizan. Estos atributos podrn ser medidos y valorados, para as comparar y extraer conclusiones objetivas. Este trabajo fue realizado
por Jos Luis Gonzlez [40] que dene la Jugabilidad como el conjunto de propiedades que describen la experiencia del jugador ante un sistema de juego determinado,
cuyo principal objetivo es divertir y entretener de forma satisfactoria y creble, ya
sea solo o en compaa.
Es importante remarcar los conceptos de satisfaccin y credibilidad. El primero
es comn a cualquier sistema interactivo. Sin embargo, la credibilidad depender del
grado en el que se pueda lograr que los jugadores se impliquen en el juego.
Hay que signicar que los atributos y propiedades que se utilizan para caracterizar
la Jugabilidad y la Experiencia del Jugador, en muchos casos ya se han utilizado para
caracterizar la Usabilidad, pero en los videojuegos presentan matices distintos. Por
ejemplo, el Aprendizaje en un videojuego puede ser elevado, lo que puede provocar
que el jugador se vea satisfecho ante el reto que supone aprender a jugarlo y, posteriormente, desarrollar lo aprendido dentro del juego. Un ejemplo lo tenemos en el
videojuego Prince of Persia, donde es difcil aprender a controlar nuestro personaje a
travs de un mundo virtual, lo que supone un reto en los primeros compases del juego.
Sin embargo, en cualquier otro sistema interactivo podra suponer motivo suciente
de rechazo. Por otro lado, la Efectividad en un juego no busca la rapidez por completar una tarea, pues entra dentro de la naturaleza del videojuego que el usuario est
jugando el mximo tiempo posible y son muchos los ejemplos que podramos citar.
Los atributos a los que hacemos referencia para caracterizar la Jugabilidad son los
siguientes:
Satisfaccin. Agrado o complacencia del jugador ante el videojuego y el proceso de jugarlo.
C20
[510]
20.1.3.
Facetas de la Jugabilidad
Uno de los objetivos, una vez denida la Jugabilidad, es poder medirla o cuanticarla. Este proceso es costoso debido a la cantidad de objetivos no funcionales que
afectan a la Experiencia del Jugador. Como plantea [40], una buena estrategia es la
de considerar una representacin de la Jugabilidad basada en facetas de la misma. La
organizacin en facetas puede considerarse una subdivisin lgica de la Jugabilidad
global en jugabilidades un poco ms especcas. Cada una de estas facetas facilitar
la identicacin y medicin de las propiedades introducidas anteriormente. Adems,
as ser ms fcil relacionar la Jugabilidad con los elementos particulares de un videojuego.
Como facetas particulares podran considerarse las siguientes, aunque no es algo
cerrado y en algn juego particular podra aparecer y proponerse alguna otra faceta
que fuese objeto de consideracin:
[511]
Jugabilidad Intrnseca. Se trata de la Jugabilidad medida en la propia naturaleza del juego y cmo se proyecta al jugador. Est ligada al diseo del Gameplay
que se describe ms adelante. La forma de valorarla pasa por analizar cmo se
representan las reglas, los objetivos, el ritmo y las mecnicas del videojuego.
Jugabilidad Mecnica. Es la Jugabilidad asociada a la calidad del videojuego
como sistema software. Est ligada a lo que sera el motor del juego, haciendo
hincapi en caractersticas como la uidez de las escenas cinemticas, la correcta iluminacin, el sonido, los movimientos grcos y el comportamiento de los
personajes del juego y del entorno, sin olvidar los sistemas de comunicacin en
videojuegos multijugador.
Jugabilidad Interactiva. Es la faceta asociada a todo lo relacionado con la
interaccin con el usuario, el diseo de la interfaz de usuario, los mecanismos
de dilogo y los sistemas de control.
Jugabilidad Artstica. Est asociada a la calidad y adecuacin artstica y esttica de todos los elementos del videojuego y a la naturaleza de ste. Entre
ellos estarn la calidad grca y visual, los efectos sonoros, la banda sonora y
las melodas del juego, la historia y la forma de narracin de sta, as como la
ambientacin realizada de todos estos elementos dentro del videojuego.
Jugabilidad Intrapersonal (o Personal). Est relacionada con la percepcin
que tiene el propio usuario del videojuego y los sentimientos que a ste le produce. Como tal, tiene un alto valor subjetivo.
Jugabilidad Interpersonal (o de Grupo). Muestra las sensaciones o percepciones de los usuarios que aparecen cuando se juega en grupo, ya sea de forma
competitiva, cooperativa o colaborativa. En relacin a cualquier sistema interactivo con soporte para grupos, se relacionara con lo que tiene que ver con
percepcin del grupo (o awareness de grupo).
C20
En [40] incluso se relacionan, a nivel interactivo, estas facetas para ilustrar cmo
pueden ser las implicaciones e inuencias que presentan. Esta relacin se resume en
la gura 20.2.
[512]
Con todo lo anterior, se puede concluir que la Jugabilidad de un juego podra considerarse como el anlisis del valor de cada una de las propiedades y de los atributos
en las facetas consideradas.
20.1.4.
[513]
C20
Figura 20.3: Clasicacin de las propiedades de la calidad del producto y del proceso en un videojuego
[514]
El modelo de Jugabilidad presentado se completa con la identicacin y asociacin de mtricas para los los factores y atributos que hemos apuntado. Estas mtricas
(ver guras 20.5 a 20.9) son consecuencia de la adaptacin de mtricas propuestas
en otros estndares internacionales pero particularizadas para el caso particular de los
videojuegos.
[515]
C20
[516]
habilidades y maneras de jugar del jugador. Por otro lado, en un videojuego hay metas
que debe realizar el jugador, pero sin embargo, la facilidad de consecucin de esas
metas no es el principal objetivo. De hecho, ms bien podra decirse que es justo lo
contrario, el uso del juego debe ser una motivacin y presentar cierta dicultad de
consecucin, de lo contrario el jugador perder motivacin por el uso del videojuego.
As mismo, la medida de la frecuencia de error en el software tradicional con
un valor cercano a 0 siempre es mejor, pero en videojuegos podemos encontrar tanto
valores cercanos a 0 como a 1. Si el valor es cercano a 0, nos encontramos ante un
jugador experto o que la dicultad del juego es muy baja. Cercano a 1, nos informa
que nos encontramos ante un jugador novato, o que se encuentra en los primeros compases del juego, o que la dicultad es muy elevada. Es por ello que los videojuegos
ofrecen distintos niveles de dicultad para atraer a los nuevos jugadores, evitando, por
ejemplo, que una dicultad extremadamente fcil haga que el juego pierda inters y
se vuelva aburrido.
La ecacia en el caso de los videojuegos es relativa, es decir, el usuario querr jugar de forma inmediata, sin perder tiempo en recibir excesiva informacin, pero, de la
misma manera que comentbamos con anterioridad, el juego debe aportar dicultad y
el usuario debera encontrar cierta resistencia y progresiva dicultad en la consecucin
de las metas que lleve asociado.
La personalizacin tambin es algo especialmente deseable en el mundo del videojuego, porque en l coexisten muchos elementos de diseo que tratan de distraer,
de acompaar y de establecer la forma de interaccin. sta ltima debera ser exible
en cuanto a poder dar soporte a diferentes formas de interactuar: teclas, mandos, sonidos, etc. El atributo de la accesibilidad, aunque deseable y exigible, tradicionalmente
no ha contado con mucha atencin en el desarrollo de videojuegos.
Este aspecto est cambiando y la presencia de este atributo contribuye al uso del
mismo ya sea en la interfaz de usuario o en las mecnicas del juego. En este modelo de
Jugabilidad este atributo se consider implcitamente dentro de otros. Los problemas
de la accesibilidad pueden considerarse problemas de Usabilidad/Jugabilidad para,
por ejemplo, jugadores con algn tipo de discapacidad.
Si un jugador no puede entender lo que se dice en determinadas escenas u or
si otro personaje camina detrs de l por problemas de sonido, es recomendable el
uso de subttulos. Si el jugador no puede manejar determinado control de juego, se
recomienda el uso de dispositivos alternativos para facilitar el control de juego.
La seguridad/prevencin es un factor que, en el caso de los videojuegos, toma
cada vez ms importancia. El juego, en la actualidad, no es slo esttico, mental y
de sobremesa, sino que supone, en algunos casos, exigencias fsicas, por ejemplo el
uso de un control de juego que demande un esfuerzo corporal o movimientos bruscos,
los cuales pueden ser potencialmente peligrosos o dainos para la salud si el jugador
desarrolla la actividad de juego con ellos durante un tiempo prolongado de ocio.
La satisfaccin es el atributo ms determinante al tratar con videojuegos. Muchos
aspectos: cognitivos, emocionales, fsicos, de conanza y sociales pueden considerarse bajo este factor de la Jugabilidad. La estimacin de la misma se realiza fundamentalmente con cuestionarios y observando al jugador mientras juega y viendo cules son
sus preferencias de un momento de ocio para el siguiente. Probablemente, este atributo en videojuegos es el ms rico y subjetivo. Por lo tanto, es el que se ha enriquecido
ms con atributos y propiedades para mejorar su medida y estimacin.
Finalmente, en la ltima columna de la tabla se proponen diversos mtodos de
evaluacin para cada mtrica. Estos mtodos pueden enriquecerse y guiarse por las
facetas, las cuales nos pueden ayudar a identicar la calidad de elementos concretos
de un videojuego segn el uso y las acciones mostradas por el conjunto de jugadores.
[517]
C20
Las principales formas de medicin son la observacin, donde podemos medir con
herramientas cmo y de qu manera acta el jugador con el videojuego, usando por
ejemplo las mtricas presentadas o usar cuestionarios o tests heursticos para preguntar
o interrogar por atributos de la Jugabilidad. Estos cuestionarios pueden ir guiados por
facetas para facilitar su anlisis.
[518]
20.2.
[519]
Pre-Produccin
C20
20.2.1.
[520]
Historia. Resumen de la historia del juego. Se realizar una primera aproximacin de la trama o la historia a desarrollar durante el juego, destacando qu se
quiere contar y cmo se pretende hacerlo. Esto se denomina storyline y storytelling respectivamente.
Bocetos. Los bocetos son diseos preliminares, fundamentalmente, de los personajes y de los escenarios por los que se desarrollar la accin del juego.
Look and Feel. A partir de los bocetos se dene el aspecto graco y artstico
del juego, colores, temas dominantes, musicalidad, tcnicas de diseo 3D 2D,
posiciones de cmaras, etc.
Interfaz de Usuario. Se apuntar la forma en la que el jugador interactuar
con el juego y con qu mecanismos contar para ello: estilos de interaccin,
metforas de interaccin, paradigma de interaccin, etc.
Objetivos: Se jan las metas del juego de acuerdo a la historia que se va a
desarrollar.
Reglas: Se establece qu acciones podr desarrollar el jugador y cmo podr
hacerlo.
[521]
Como se ha indicado anteriormente, esta primera versin del GDD ser el punto
de partida para iniciar la fase de Produccin, pero cabe insistir sobre la importancia de
uno de sus elementos: se trata del Gameplay. Dado el carcter un tanto difuso de este
concepto, consideremos como ejemplo el caso particular del conocido y clsico juego Space Invaders. En este juego indicaramos que se debe poder mover una nave
alrededor del cuadrante inferior de la pantalla y disparar a una serie de enemigos que
aparecen por la parte superior de la pantalla y que desaparecen cuando son alcanzados
por los disparos. Estos enemigos tratan de atacarnos con sus disparos y presionndonos mediante la reduccin de nuestro espacio de movimientos e intentando chocar
contra nuestra nave.
El Gameplay tiene una implicacin importantsima en la calidad nal del juego
y, por lo extensin, en la Jugabilidad del mismo. Luego los esfuerzos destinados a su
anlisis y planteamiento revertirn directamente en las propiedades que caracterizan la
Juabilidad. No obstante y para profundizar en ms detalle sobre este aspecto, se recomienda consultar los siguientes libros: Rules of Play: Game Design Fundamentals
[80] y Game Design: Theory and Practice [77].
20.2.2.
Produccin
La fase de Produccin es la fase donde se concentra el trabajo principal, en volumen y en nmero de participantes, del proceso de desarrollo del videojuego, especialmente en lo que se denomina Diseo del Juego y Diseo Tcnico. Hay que signicar
que este curso est orientado, fundamentalmente, a las tareas y tcnicas relacionadas
con el Diseo Tcnico, pero no queremos dejar de situarlo en el contexto del proceso
global que requiere llevarse a cabo para concebir y desarrollar un producto de estas
caractersticas.
C20
Presupuesto. Se realizar una primera aproximacin al presupuesto que soportar el proyecto de desarrollo del videojuego.
[522]
[523]
Gold Master. Esta etapa aborda una prueba denitiva con el producto nal que
se publicar y que se producir. Obviamente, incluye todo el contenido artstico,
tcnico y documental (es decir, los manuales de usuario). En este momento, la
publicidad deber ser la mayor posible, incluyndose la realizacin de reportajes,
artculos, etc.
20.2.3.
Post-Produccin
C20
[524]
20.3.
Metodologas Alternativas
20.3.1.
20.3.2.
Desarrollo Incremental
20.3.3.
[525]
Esta metodologa plantea la utilizacin de procesos giles de desarrollo de software, unido a los pilares bsico de la metodologa de desarrollo de productos Scrum
[95].
El objetivo principal del mtodo de Keith es hacer un diseo centrado en el jugador
y en los resultados del proceso de desarrollo en cada una de sus fases. As, se resalta
la importancia de obtener la opinin del usuario en cada momento, por lo que intenta
involucrar al equipo de pruebas lo antes posible. De esta forma, se facilitar la posibilidad detectar y solucionar a tiempo todos los posibles errores y se podr analizar
la Juabilidad en cada momento para ir mejorndola continuamente, del mismo modo
que se hace para el caso particular de la Usabilidad en un sistema interactivo.
Esta metodologa requiere de la realizacin de importantes esfuerzos iniciales para
lograr obtener prototipos bsicos pero jugables y, por lo tanto, evaluables. Con estos
prototipos se inicia un proceso iterativo en el que el equipo de pruebas lo utiliza y
proporciona realimentacin orientada a la mejora, especialmente de la Jugabilidad
pero tambin de otros detalles que pueden caracterizar el producto nal.
Informacin mucho ms detallada de cmo aplicar esta metodologa puede encontrarse en el libro Agile Game Development with Scrum de [55].
20.3.4.
En esta subseccin se va a describir la propuesta de [40] que est inspirada directamente en los principios fundamentales del DCU (Diseo Centrado en el Usuario) y
de las metodologas de desarrollo software que se han derivado de los mismos.
C20
La idea fundamental del DCU, como ya se ha apuntado anteriormente, es la involucrar al usuario y hacerlo al principio de cualquier proceso de desarrollo, ya que
muchos de los problemas del software se deben a una carencia en las fases iniciales
del desarrollo, concretamente en las fases de elicitacin y de anlisis de requisitos.
Esto ya ha sido contemplado en diversos estndares que plantean ciclos de vida del
proceso que incluyen modelos de madurez para la Usabilidad como pilar fundamental
que garantizar el xito del producto en cuanto a la Experiencia del Usuario.
[526]
Figura 20.13: Comparacin entre el mtodo de Diseo Centrado en el Jugador y el de Diseo Centrado en
el Usuario de MPIu+a
Captulo
21
C++ Avanzado
Francisco Moya Fernndez
David Villa Alises
Sergio Prez Camacho
Cleto Martn Angelina
21.1.
527
[528]
Como ya hemos puntualizado en captulos anteriores, es muy importante no optimizar de manera prematura. Para ilustrar este aspecto veamos el siguiente ejemplo
tomado de [17], captulo 11.
Anti-optimizaciones
Listado 21.1: Dos formas de sumar enteros
1
2
3
4
5
6
7
8
9
10
11
12
21.1.1.
rboles binarios
Las estructuras arborescentes se encuentran entre las ms utilizadas en la programacin de todo tipo de aplicaciones. Ya hemos visto en el mdulo 2 algunas de sus
aplicaciones para el mezclado de animaciones (Priority Blend Tree), o para indexar
el espacio (BSP Tree, quatree, octree, BBT). Estudiaremos su funcionamiento en este
captulo, pero el desarrollo de videojuegos no se limita a los grcos, por lo que otro
tipo de rboles ms generales pueden resultar tambin necesarios.
Los rboles se utilizan con frecuencia como mecanismo eciente de bsqueda.
Para este n implementan un rico conjunto de operaciones: bsqueda de un elemento,
mnimo o mximo, predecesor o sucesor de un elemento y las clsicas operaciones
de insercin y borrado. Se pueden emplear como un diccionario o como una cola con
prioridad.
Todas estas operaciones estn presentes en los contenedores ordenados de la STL,
singularmente set, multiset, map y multimap. No debe extraar por tanto que
en todos ellos se emplea una variante de rbol binario denominada red-black tree.
Un nodo de rbol contiene habitualmente un atributo key que se emplea para
compararlo con otros nodos y adems mantiene un conjunto de punteros a otros nodos
que mantienen su relacin con el resto de la estructura. As, por ejemplo, los nodos de
rboles binarios mantienen un atributo parent que apunta al nodo padre, y un par
de punteros left y right que apuntan al hijo por la izquierda y por la derecha respectivamente. A su vez cada hijo puede tener otros nodos hijos, por lo que realmente cada
nodo cuenta con dos subrboles (izquierdo y derecho).
[529]
10
4
1
17
5
16
4
21
10
5
17
16
21
Figura 21.1: Dos rboles de bsqueda binaria. Ambos contienen los mismos elementos pero el de la izquierda es mucho ms eciente
C21
[530]
Para obtener el mnimo basta recorrer todos los subrboles de la izquierda y anlogamente para encontrar el mximo hay que recorrer todos los subrboles de la derecha
hasta llegar a un nodo sin subrbol derecho.
Listado 21.4: Bsqueda del elemento mximo en un rbol de bsqueda binaria
1
2
3
4
5
Las instancias de Node no tienen por qu ser visibles directamente al programador, al igual que los contenedores tipo set de la STL. Por ejemplo, esto puede lograrse
utilizando un namespace privado.
La bsqueda de un elemento tambin puede plantearse con un algoritmo recursivo
aprovechando la propiedad que dene a los rboles de bsqueda binaria:
Listado 21.5: Bsqueda de una clave en un rbol de bsqueda binaria
1
2
3
4
5
[531]
El atributo root mantiene cul es el nodo raz del rbol. Los mtodos de insercin
y borrado deben actualizarlo adecuadamente.
Listado 21.8: Insercin en un rbol de bsqueda binaria
void insert(NodeType* z) {
NodeType* y = 0;
NodeType* x = root;
while (x != 0) {
y = x;
if (z->key < x->key)
x = x->left;
else
x = x->right;
}
z->parent = y;
if (y == 0) root = z;
else if (z->key < y->key)
y->left = z;
else
y->right = z;
}
Bsicamente replica el procedimiento de bsqueda para encontrar el hueco donde debe insertar el elemento, manteniendo el padre del elemento actual para poder
recuperar el punto adecuado al llegar a un nodo nulo.
El procedimiento ms complejo es el de borrado de un nodo. De acuerdo a [20] se
identican los cuatro casos que muestra la gura 21.2. Un caso no representado es el
caso trivial en el que el nodo a borrar no tenga hijos. En ese caso basta con modicar
el nodo padre para que el hijo correspondiente sea el objeto nulo. Los dos primeros
casos de la gura corresponden al borrado de un nodo con un solo hijo, en cuyo caso el
hijo pasa a ocupar el lugar del nodo a borrar. El tercer caso corresponde al caso en que
el hijo derecho no tenga hijo izquierdo o el hijo izquierdo no tenga hijo derecho, en
cuyo caso se puede realizar la misma operacin que en los casos anteriores enlazando
adecuadamente las dos ramas. El cuarto caso corresponde al caso general, con dos
hijos no nulos. En ese caso buscamos un sucesor del subrbol izquierdo que no tenga
hijo izquierdo, que pasa a reemplazar al nodo, reajustando el resto para mantener la
condicin de rbol de bsqueda binaria.
Con el objetivo de facilitar el movimiento de subrboles denimos el mtodo
transplant(). El subrbol con raz u se reemplaza con el subrbol con raz v.
C21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[532]
void remove(NodeType* z) {
if (z->left == 0)
transplant(z, z->right);
else if (z->right == 0)
transplant(z, z->left);
else {
NodeType* y = NodeType::minimum(z->right);
if (y->parent != z) {
transplant(y, y->right);
y->right = z->right;
y->right->parent = y;
}
transplant(z, y);
y->left = z->left;
y->left->parent = y;
}
}
[533]
(a)
r
r
NIL
(b)
l
NIL
(c)
y
y
NIL
(d)
z
r
NIL
q
y
y
r
NIL
C21
Figura 21.2: Casos posibles segn [20] en el borrado de un nodo en un rbol de bsqueda binaria
[534]
10
4
17
1
NIL
5
NIL
NIL
16
NIL
NIL
21
NIL
NIL
NIL
Figura 21.3: Ejemplo de rbol rojo-negro. Los nodos hoja no se representarn en el resto del texto.
[535]
Listado 21.12: Bsqueda del mayor elemento en un rbol rojo-negro.
1
2
3
4
rotate right
y
x
a
rotate left
x
y
a
b
La operacin dual rotate_right() puede implementarse simplemente intercambiando en el algoritmo anterior x por y, y left por right.
La insercin puede ahora realizarse de una forma muy parecida al caso general
asumiendo que el color del nodo a insertar es rojo y despus arreglando el rbol con
rotaciones y cambios de color.
Listado 21.14: Insercin en un rbol rojo-negro
1
2
3
4
5
6
7
8
void insert(NodeType* z) {
NodeType* y = NodeType::nil();
NodeType* x = root;
while (x != NodeType::nil()) {
y = x;
if (z->key < x->key)
x = x->left;
else
C21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[536]
9
10
11
12
13
14
15
16
17
18
19
20
21
22
x = x->right;
}
z->parent = y;
if (y == NodeType::nil())
root = z;
else if (z->key < y->key)
y->left = z;
else
y->right = z;
z->left = NodeType::nil();
z->right = NodeType::nil();
z->color = Node::Color::Red;
insert_fixup(z);
Al asumir el color rojo podemos haber violado las reglas de los rboles rojo-negro.
Por esta razn llamamos a la funcin insert_fixup() que garantiza el cumplimiento de las reglas tras la insercin:
Listado 21.15: Reparacin tras la insercin en rbol rojo-negro
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
void insert_fixup(NodeType* z) {
while (z->parent->color == Node::Color::Red) {
if (z->parent == z->parent->parent->left) {
NodeType* y = z->parent->parent->right;
if (y->color == Node::Color::Red) {
z->parent->color = Node::Color::Black;
y->color = Node::Color::Black;
z->parent->parent->color = Node::Color::Red;
z = z->parent->parent;
}
else {
if (z == z->parent->right) {
z = z->parent;
rotate_left(z);
}
z->parent->color = Node::Color::Black;
z->parent->parent->color = Node::Color::Red;
rotate_right(z->parent->parent);
}
}
else {
NodeType* y = z->parent->parent->left;
if (y->color == Node::Color::Red) {
z->parent->color = Node::Color::Black;
y->color = Node::Color::Black;
z->parent->parent->color = Node::Color::Red;
z = z->parent->parent;
}
else {
if (z == z->parent->left) {
z = z->parent;
rotate_right(z);
}
z->parent->color = Node::Color::Black;
z->parent->parent->color = Node::Color::Red;
rotate_left(z->parent->parent);
}
}
}
root->color = Node::Color::Black;
}
[537]
La insercin de un nodo rojo puede violar la regla 2 (el nodo raz queda como
rojo en el caso de un rbol vaco) o la regla 4 (el nodo insertado pasa a ser hijo de
un nodo rojo). Este ltimo caso es el que se contempla en el bucle de la funcin
insert_fixup(). Cada una de las dos ramas del if sigue la estrategia dual, dependiendo de si el padre es un hijo derecho o izquierdo. Basta estudiar el funcionamiento
de una rama, dado que la otra es idntica pero intercambiando right y left. Bsicamente se identican tres casos.
El primero corresponde a las lneas 6 a 9 . Es el caso en que el nodo a insertar
pasa a ser hijo de un nodo rojo cuyo hermano tambin es rojo (e.g. gura 21.5.a).
En este caso el nodo padre y el nodo to se pasan a negro mientras que el abuelo
se pasa a rojo (para mantener el nmero de nodos negros en todos los caminos).
Al cambiar a rojo el nodo abuelo es posible que se haya vuelto a violar alguna
regla, y por eso se vuelven a comprobar los casos.
Otra posibilidad es que el nodo to sea negro y adems el nodo insertado sea hijo
derecho (e.g. gura 21.5.b). En ese caso se realiza una rotacin a la izquierda
para reducirlo al caso siguiente y se aplica lo correspondiente al ltimo caso.
El ltimo caso corresponde a que el nodo to sea negro y el nodo insertado sea
hijo izquierdo (e.g. gura 21.5.c). En ese caso se colorea el padre como negro, y
el abuelo como rojo, y se rota a la derecha el abuelo. Este mtodo deja un rbol
correcto.
(a)
(b)
(c)
C21
[538]
void remove(NodeType* z) {
NodeType* y = z;
NodeType* x;
NodeType::Color y_orig_color = y->color;
if (z->left == NodeType::nil()) {
x = z->right;
transplant(z, z->right);
}
else if (z->right == NodeType::nil()) {
x = z->left;
transplant(z, z->left);
}
else {
y = Node::minimum(z->right);
y_orig_color = y->color;
x = y->right;
if (y->parent == z) {
x->parent = y;
}
else {
transplant(y, y->right);
y->right = z->right;
y->right->parent = y;
}
transplant(z, y);
y->left = z->left;
y->left->parent = y;
y->color = z->color;
}
if (y_orig_color == Node::Color::Black)
rb_remove_fixup(x);
}
El nodo y corresponde al nodo que va a eliminarse o moverse dentro del rbol. Ser
el propio z si tiene menos de dos hijos o el nodo y de los casos c y d en la gura 21.2.
Mantenemos la variable y_orig_color con el color que tena ese nodo que se ha
eliminado o movido dentro del rbol. Solo si es negro puede plantear problemas de
violacin de reglas, porque el nmero de nodos negros por cada rama puede variar.
Para arreglar los problemas potenciales se utiliza una funcin anloga a la utilizada en
la insercin de nuevos nodos.
Listado 21.18: Reparacin tras borrar un nodo en rboles rojo-negro
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void remove_fixup(NodeType* x) {
while (x != root && x->color == Node::Color::Black) {
if (x == x->parent->left) {
NodeType* w = x->parent->right;
if (w->color == Node::Color::Red) {
w->color = Node::Color::Black;
x->parent->color = Node::Color::Red;
rotate_left(x->parent);
w = x->parent->right;
}
if (w->left->color == Node::Color::Black
&& w->right->color == Node::Color::Black) {
w->color = Node::Color::Red;
x = x->parent;
}
else {
if (w->right->color == Node::Color::Black) {
w->left->color = Node::Color::Black;
[539]
w->color = Node::Color::Red;
rotate_right(w);
w = x->parent->right;
}
w->color = x->parent->color;
x->parent->color = Node::Color::Black;
w->right->color = Node::Color::Black;
rotate_left(x->parent);
x = root;
}
}
else {
NodeType* w = x->parent->left;
if (w->color == Node::Color::Red) {
w->color = Node::Color::Black;
x->parent->color = Node::Color::Red;
rotate_right(x->parent);
w = x->parent->left;
}
if (w->right->color == Node::Color::Black
&& w->left->color == Node::Color::Black) {
w->color = Node::Color::Red;
x = x->parent;
}
else {
if (w->left->color == Node::Color::Black) {
w->right->color = Node::Color::Black;
w->color = Node::Color::Red;
rotate_left(w);
w = x->parent->left;
}
w->color = x->parent->color;
x->parent->color = Node::Color::Black;
w->left->color = Node::Color::Black;
rotate_right(x->parent);
x = root;
}
}
}
x->color = Node::Color::Black;
C21
[540]
(a)
B
x A
D
C
w
E
x A
w C
(b)
x B
x A
D w
C
(c)
B
x A
D
C
x A
D
E
(d)
B
x A
D
C
w
E
B
A
E
C
AVL trees
Los rboles AVL (Adelson-Velskii and Landis) son otra forma de rbol balanceado
en el que se utiliza la altura del rbol como criterio de balanceo. Solo puede haber una
diferencia de 1 entre la altura de dos ramas. Es por tanto un criterio ms estricto que
los red-black trees, lo que lo hace menos eciente en las inserciones y borrados pero
ms eciente en las lecturas.
rboles balanceados
Los red-black trees son ms ecientes en insert() y remove(),
pero los AVL trees son ms ecientes en search().
[541]
Cada nodo tiene informacin adicional con la altura del rbol en ese punto. En
realidad tan solo es necesario almacenar el factor de equilibrio que es simplemente
la diferencia entre las alturas del subrbol izquierdo y el derecho. La ventaja de esta
ltima alternativa es que es un nmero mucho ms reducido (siempre comprendido en
el rango -2 a +2) por lo que puede almacenarse en solo 3 bits.
Para insertar elementos en un rbol AVL se utiliza un procedimiento similar a
cualquier insercin en rboles de bsqueda binaria, con dos diferencias:
La insercin debe computar el factor de equilibrio en los nodos afectados.
Finalmente hay que equilibrar el rbol si es necesario.
El equilibrado se realiza con rotaciones siguiendo el procedimiento representado
en la gura 21.7. Es importante destacar que las propias funciones de rotacin alteran
los factores de equilibrio de los nodos involucrados (nodos x e y en la gura 21.4).
Radix tree
C21
An hay otro tipo de rboles binarios que vale la pena comentar por sus implicaciones con los videojuegos. Se trata de los rboles de prejos, frecuentemente llamados
tries1 .
[542]
(a)
h+2
h+2
B
C
rotate left(B)
rotate right(B)
(b)
h+2
h+2
rotate right(A)
rotate left(A)
(c)
h+1
h+1
h+1
h+1
En los rboles de prejos la posicin de los nodos est prejada a priori por el
valor de la clave. Estos rboles no realizan ninguna funcin de equilibrado por lo que
su implementacin es trivial. Sin embargo estarn razonablemente equilibrados si los
nodos presentes estn uniformemente repartidos por el espacio de claves.
21.1.2.
Recorrido de rboles
En multitud de ocasiones es necesario recorrer los elementos de un rbol en un orden determinado. Son frecuentes los recorridos en orden, en preorden, y en postorden.
El recorrido en orden sigue el orden del campo clave. Es decir, para cualquier
nodo primero se visitan los nodos del subrbol izquierdo, luego el nodo y nalmente
los nodos del subrbol derecho.
[543]
1
10
1
011
100
1
1011
Figura 21.8: Un ejemplo de trie extraido de [20]. Contiene los elementos 1011, 10, 011, 100 y 0.
Listado 21.19: Recorrido en orden en un rbol de bsqueda binaria
1
2
3
4
5
6
7
8
9
10
11
12
C21
[544]
3
4
5
6
7
8
9
10
11
12
postorder_tree_walk(root, f);
Pero para el recorrido de estructuras de datos con frecuencia es mucho mejor emplear el patrn iterador. En ese caso puede reutilizarse cualquier algoritmo de la STL.
Incluir el orden de recorrido en el iterador implica almacenar el estado necesario.
Las funciones de recorrido anteriormente descritas son recursivas, por lo que el estado se almacenaba en los sucesivos marcos de pila correspondientes a cada llamada
anidada. Por tanto necesitamos un contenedor con la ruta completa desde la raz hasta el nodo actual. Tambin tendremos que almacenar el estado de recorrido de dicho
nodo, puesto que el mismo nodo es visitado en tres ocasiones, una para el subrbol
izquierdo, otra para el propio nodo, y otra para el subrbol derecho.
Listado 21.22: Iterador en orden en un rbol de bsqueda binaria
1 class inorder_iterator : public std::iterator<std::
input_iterator_tag,
2
3
4
5
6
7
Node<KeyType>,
ptrdiff_t,
const Node<KeyType>*,
const Node<KeyType>&>
{
8
9
10 public:
11
inorder_iterator(NodeType* x) {
12
_current.push_back(std::make_pair(x,VisitingLeft));
13
goToNextNode();
14
}
15
16
const NodeType& operator*() const {
17
return *_current.back().first;
18
}
19
20
const NodeType* operator->() const {
21
return _current.back().first;
22
}
23
24
bool equal(inorder_iterator<KeyType> const& rhs) const {
25
return *this == rhs;
26
}
27
28
inorder_iterator<KeyType>& operator++() {
29
goToNextNode();
30
}
31
32
inorder_iterator<KeyType> operator++(int) {
33
inorder_iterator<KeyType> ret(*this);
34
goToNextNode();
35
return ret;
36
}
37
38 private:
39
void goToNextNode();
40 };
[545]
41
42 template<typename KeyType>
43 inline bool operator== (inorder_iterator<KeyType> const& lhs,
44
inorder_iterator<KeyType> const& rhs) {
45
return lhs.equal(rhs);
46 }
void
inorder_iterator<KeyType>::goToNextNode()
{
if (_current.empty()) return;
std::pair<NodeType*,IteratorState>& last = _current.back();
if (last.second == VisitingLeft) {
NodeType* l = last.first->left;
if (l == 0) last.second = VisitingNode;
else {
_current.push_back(std::make_pair(l,VisitingLeft));
goToNextNode();
}
}
else if (last.second == VisitingNode) {
NodeType* r = last.first->right;
if (r == 0) _current.pop_back();
else {
last.second = VisitingRight;
_current.push_back(std::make_pair(r,VisitingLeft));
}
goToNextNode();
}
else if (last.second == VisitingRight) {
_current.pop_back();
goToNextNode();
}
21.1.3.
Quadtree y octree
C21
[546]
Cuando se utilizan para indexar segmentos o polgonos puede ocurrir que un mismo segmento cruce el lmite de un cuadrante o un octante. En ese caso existen dos
posibles soluciones:
Hacer un recortado (clipping) del polgono dentro de los lmites del cuadrante
u octante.
Poner el polgono en todos los cuadrantes u octantes con los que intersecta.
En este ltimo caso es preciso disponer de alguna bandera asociada a los polgonos
para no recorrerlos ms veces de las necesarias.
Simon Perreault distribuye una implementacin sencilla y eciente de octrees en
C++2 . Simplicando un poco esta implementacin los nodos son representados de esta
forma:
Listado 21.24: Representacin de nodos en un octree.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[547]
17
18
19
20
21
22
23
24
25
26
27
28
29
30
};
Node* children[2][2][2];
Esta representacin de rboles diferencia entre nodos hoja y nodos de ramicacin. Los valores solo se almacenan en los nodos hoja y stos no tienen la sobrecarga
de los punteros a los ocho subrboles. Por contra, los nodos de ramicacin no tienen
sobrecarga de valores asociados, puesto que para la insercin de un elemento puede
ser necesario aadir un nmero de nodos de ramicacin sin valor alguno.
El uso bsico es muy sencillo. El contenido a incluir puede ser cualquier cosa,
desde simples valores (color de un punto), pasando por un voxel (pixel 3D) hasta
polgonos o poliedros. El tipo de contenido puede ser tambin una referencia a un
objeto grco. De esta forma se podra incluir el mismo elemento (e.g. un polgono) en
mltiples nodos hoja. En el captulo siguiente veremos cmo la tcnica de referencias
con contador puede ayudar en casos como ste.
Listado 21.25: Representacin de nodos en un octree.
1
2
3
4
5
6
7
8
#include "octree.h"
int main()
{
Octree<double> o(4096);
o(1,2,3) = 3.1416;
o.erase(1,2,3);
}
La lnea 5 construye un octree de 4096 puntos de ancho en cada dimensin.
Esta implementacin requiere que sea una potencia de dos, siendo posible indexar
4096 4096 4096 nodos.
La regularidad de los octree los hacen especialmente indicados para la paralelizacin con GPU y recientemente estn teniendo cierto resurgimiento con su
utilizacin en el renderizado de escenas con raycasting o incluso raytracing
en una tcnica denominada Sparse Voxel Octree.
C21
[548]
21.2.
21.2.1.
Smart pointers
Los punteros inteligentes (smart pointers) son tipos de datos que simplican de
alguna manera la gestin de la memoria dinmica. Facilitan la gestin del ciclo de
vida de las variables dinmicas para evitar los problemas frecuentemente asociados a
los punteros, especialmente la liberacin de la memoria.
La biblioteca estndar de C++ incorpora una plantilla denominada auto_ptr. Su
objetivo es envolver un puntero normal de tal forma que la destruccin del puntero lleve consigo tambin la destruccin del objeto apuntado. Por lo dems, un auto_ptr
se comporta como si se tratara del propio puntero. Por ejemplo, es frecuente encontrar
cdigo como el que sigue:
Listado 21.26: Ejemplo de uso inseguro de punteros.
1
2
3
4
5
T* p = new T();
// cuerpo de la funcin
delete p;
[549]
Como puede verse hemos ligado el tiempo de vida del objeto construido en memoria dinmica al tiempo de vida del auto_ptr, que suele ser una variable automtica o
un miembro de clase. Se dice que el auto_ptr posee al objeto dinmico. Pero puede
ceder su posesin simplemente con una asignacin o una copia a otro auto_ptr.
Listado 21.28: Cesin de la posesin del objeto dinmico.
1
2
3
4
5
6
7
auto_ptr<T> q(p);
auto_ptr<T> r;
p->f(); // error (NULL ref)
q->f(); // ok
r = q;
q->f(); // error (NULL ref)
r->f(); // ok
Es decir, auto_ptr garantiza que solo hay un objeto que posee el objeto dinmico. Tambin permite desligar el objeto dinmico del auto_ptr para volver a
gestionar la memoria de forma explcita.
Listado 21.29: Recuperacin de la propiedad del objeto dinmico.
1
2
T* s = r.release();
delete s;
Nunca se deben usar auto_ptr en contenedores estndar, porque los contenedores de la STL asumen una semntica de copia incompatible con la del auto_ptr.
La copia de un auto_ptr no genera dos objetos equivalentes.
Esta limitacin, que no es detectada en tiempo de compilacin, es una de las
motivaciones de un completo rediseo de esta funcionalidad para el estndar C++
de 2011. An sigue soportando auto_ptr pero se desaconseja su uso en favor de
unique_ptr. El nombre deriva de que, al igual que auto_ptr, garantiza que solo
un unique_ptr puede estar apuntando a un mismo recurso. Sin embargo, a diferencia de auto_ptr no es posible copiarlos. Sin embargo existe la posibilidad de
transferencia de propiedad entre unique_ptr utilizando la nueva semntica de movimiento del estndar C++11.
1
2
3
4
C21
[550]
La funcin f() devuelve memoria dinmica. Con simples punteros eso implicaba
que el llamante se haca cargo de su destruccin, de controlar su ciclo de vida. Con esta
construccin ya no es necesario. Si el llamante ignora el valor de retorno ste se libera
automticamente al destruir la variable temporal correspondiente al valor de retorno.
Si en cambio el llamante asigna el valor de retorno a otra variable unique_ptr
entonces est asumiendo la propiedad y se liberar automticamente cuando el nuevo
unique_ptr sea destruido.
Las nuevas caractersticas de la biblioteca estndar para la gestin del ciclo de vida de la memoria dinmica estn ya disponibles en los compiladores libres GCC y clang. Tan solo hay que utilizar la opcin de compilacin
-std
c++0x.
Tanto con auto_ptr como con unique_ptr se persigue que la gestin de memoria dinmica sea anloga a la de las variables automticas con semntica de copia.
Sin embargo no aprovechan la posibilidad de que el mismo contenido de memoria sea
utilizado desde varias variables. Es decir, para que la semntica de copia sea la natural
en los punteros, que se generen dos objetos equivalentes, pero sin copiar la memoria
dinmica. Para ese caso el nico soporte que ofreca C++ hasta ahora eran los punteros
y las referencias. Y ya sabemos que ese es un terreno pantanoso.
La biblioteca estndar de C++11 incorpora dos nuevas plantillas para la gestin
del ciclo de vida de la memoria dinmica que ya existan en la biblioteca Boost:
shared_ptr y weak_ptr. Ambos cooperan para disponer de una gestin de memoria muy exible. La plantilla shared_ptr implementa una tcnica conocida como conteo de referencias.
Cuando se asigna un puntero por primera vez a un shared_ptr se inicializa
un contador interno a 1. Este contador se almacena en memoria dinmica y es compartido por todos los shared_ptr que apunten al mismo objeto. Cuando se asigna
este shared_ptr a otro shared_ptr o se utiliza el constructor de copia, se incrementa el contador interno. Cuando se destruye un shared_ptr se decrementa
el contador interno. Y nalmente cuando el contador interno llega a 0, se destruye
automticamente el objeto dinmico.
Listado 21.32: Ejemplos de uso de shared_ptr.
1
2
3
4
5
6
[551]
7
8
}
// ...
En la lnea 1 se construye un shared_ptr que apunta a un
objeto dinmico. Esto pone el contador interno de referencias a 1. En la lnea 4 se asigna este
shared_ptr a otro. No se copia el objeto dinmico, sino solo su direccin y la del
contador de referencias,
que adems es automticamente incrementado (pasa a valer 2). En la lnea 5 se utiliza el constructor de copia de otro shared_ptr, que
nuevamente copia solo el puntero y el puntero al contador
de referencias, adems de
incrementar su valor (pasa a valer 3). En la lnea 7 se destruye automticamente r,
con lo que se decrementa el contador de referencias (vuelve a valer 2). Cuando acabe
el bloque en el que se han declarado p y q se destruirn ambas variables, y con ello se
decrementar dos veces el contador de referencias. Al llegar a 0 automticamente se
invocar el operador delete sobre el objeto dinmico.
El conteo de referencias proporciona una poderosa herramienta para simplicar
la programacin de aplicaciones con objetos dinmicos. Los shared_ptr pueden
copiarse o asignarse con total libertad y con una semntica intuitiva. Pueden emplearse
en contenedores de la STL y pasarlos por valor libremente como parmetros a funcin
o como valor de retorno de una funcin. Sin embargo no estn totalmente exentos de
problemas. Considera el caso en el que main() tiene un shared_ptr apuntando
a una clase A y sta a su vez contiene directa o indirectamente un shared_ptr que
vuelve a apuntar a A. Tendramos un ciclo de referencias y el contador de referencias
con un balor de 2. En caso de que se destruyera el shared_ptr inicial seguiramos
teniendo una referencia a A, por lo que no se destruir.
Para romper los ciclos de referencias la biblioteca estndar incluye la plantilla
weak_ptr. Un weak_ptr es otro smart pointer a un objeto que se utiliza en estas
condiciones:
1. Solo se necesita acceso al objeto si existe.
2. Puede ser borrado por otros en cualquier momento.
3. Debe ser destruido tras su ltimo uso.
Bjarne Stroustrup 5 pone un ejemplo que tiene mucho que ver con la programacin
de videojuegos. Consideremos el caso del juego de los asteroides. Todos los asteroides
son posedos por el juego pero cada asteroide tiene que seguir los movimientos de
los asteroides vecinos para detectar colisiones. Una colisin lleva a la destruccin
de uno o ms asteroides. Cada asteroide debe almacenar una lista de los asteroides
vecinos. Pero el hecho de pertenecer a esa lista no mantiene al asteroide vivo. Por
tanto el uso de shared_ptr sera inapropiado. Por otro lado un asteroide no debe ser
destruido mientras otro asteroide lo examina (para calcular los efectos de una colisin,
por ejemplo), pero debe llamarse al destructor en algn momento para liberar los
recursos asociados. Necesitamos una lista de asteroides que podran estar vivos y una
forma de sujetarlos por un tiempo. Eso es justo lo que hace weak_ptr.
Listado 21.33: Esquema de funcionamiento del propietario de los asteroides. Usa shared_ptr
para representar propiedad.
1
2
3
4
5
vector<shared_ptr<Asteroid>> va(100);
for (int i=0; i<va.size(); ++i) {
// ... calculate neighbors for new asteroid ...
va[i].reset(new Asteroid(weak_ptr<Asteroid>(va[neighbor])))
;
launch(i);
5 http://www.research.att.com/~bs/C++0xFAQ.html#std-weak_ptr
C21
[552]
6
7
if (shared_ptr<Asteroid> q = p.lock()) {
// ... Asteroid still alive: calculate ...
}
else {
// ... oops: Asteroid already destroyed
}
Para entender por qu la linea 10 es peligrosa basta saber que el orden de evaluacin de los argumentos no est especicado. Podra evaluarse primero el operador
new, despus llamarse a la funcin g(), y nalmente no llamarse nunca al constructor
de shared_ptr porque g() eleva una excepcin.
6 Este caso
[553]
En la mayora de las bibliotecas de relativa complejidad encontramos algn tipo de smart pointer. En Ogre ya hemos visto Ogre::SharedPtr,
en ZeroC Ice hemos visto IceUtil::Handle, en Boost hay una amplia coleccin de smart pointers que incluye boost::shared_ptr y
boost::unique_ptr. Ahora que el nuevo estndar C++ incluye conteo
de referencias veremos una progresiva evolucin de las bibliotecas para adoptar la versin estndar. Mientras tanto, es muy importante utilizar en cada
biblioteca los mecanismos que incluye y no mezclarlos con otras bibliotecas.
1
2
3
4
5
6
7
8
class C {
public:
C();
/*...*/
private:
class CImpl;
auto_ptr<CImpl> pimpl_;
};
C21
[554]
21.2.2.
Command
El patrn command (se traducira como orden en castellano) se utiliza frecuentemente en interfaces grcas para el manejo de las rdenes del usuario. Consiste en
encapsular las peticiones en objetos que permiten desacoplar la emisin de la orden
de la recepcin, tanto desde el punto de vista lgico como temporal.
Problema
Existe un gran nmero de situaciones en las que la sincrona inherente a la invocacin directa a mtodos resulta poco conveniente:
La invocacin directa solamente involucra a emisor y receptor de la orden, por
lo que resulta complicado trazar la actividad del sistema en otros componentes (barras de progreso, capacidad de deshacer las rdenes ejecutadas, ayuda
contextual, etc.).
En algunas ocasiones es necesario un modelo de ejecucin transaccional, o con
limitaciones de orden. As, por ejemplo si se ejecuta una accin tambin deben
ejecutarse todas las acciones relacionadas. Y si no se ejecuta una accin deben
deshacerse todas las relacionadas. Las acciones sobre un mundo virtual (e.g.
un MMORPG) deben garantizar la ejecucin en orden causal para todos los
jugadores (la causa precede al efecto).
En ocasiones conviene grabar y reproducir una secuencia de rdenes (e.g para
la implementacin de macros o simplemente para la prueba del juego).
Muchas acciones conllevan la interaccin con el usuario en forma de wizards o
cuadros de dilogo para congurar la accin. El patrn command permite que
el objeto orden sea creado en el momento de mostrar el wizard, que el usuario
congure el objeto mediante la interaccin con el wizard, y nalmente, al cerrar
el wizard se desencadena el proceso de emisin del mensaje. De esta forma la
orden no necesita nada de cdigo de interfaz de usuario.
La mayora de los juegos actuales son programas multi-hilo. Las rdenes pueden ser generadas desde multitud de hilos, y el procesamiento de stas puede
corresponder a otro conjunto de hilos diferente. El patrn command proporciona
un mtodo sencillo para desacoplar productores y consumidores de rdenes.
En los juegos en red necesitamos ejecutar rdenes en todos los ordenadores
participantes. El patrn command facilita la serializacin de las rdenes sin ms
que serializar los objetos que las representan.
Muchos juegos aaden algn tipo de consola para interactuar directamente con
el motor empleando un intrprete de rdenes. El patrn command permite sintetizar rdenes en el juego como si se hubieran producido en el propio juego, lo
que facilita enormemente la prueba y depuracin.
[555]
Solucin
La gura 21.12 muestra un diagrama de clases con las entidades involucradas. El
cliente es el que crea los objeto command concretos y los asocia con el receptor de la
accin. Posteriormente, y de forma totalmente desacoplada, un invocador llamar al
mtodo execute() de cada objeto orden creado. Los objetos command concretos
implementan el mtodo execute(), normalmente delegando total o parcialmente
sobre el receptor de la accin.
Un ejemplo de aplicacin en un videojuego podra ser el que se muestra en la
gura 21.14.
C21
El interfaz de usuario crea las rdenes a realizar por el personaje o los personajes
que estn siendo controlados, as como la asociacin con su personaje. Estas acciones
se van procesando por el motor del juego, posiblemente en paralelo.
Implementacin
En trminos generales el patrn command permite descargar ms o menos inteligencia sobre el objeto ConcreteCommand. Se juega entre los dos posibles extremos.
[556]
Entre estos dos extremos se encuentran las rdenes que realizan algunas funciones pero delegan otras en el receptor. En general a todo este tipo de rdenes se les
denomina active commands.
Desde el punto de vista de la implementacin hay poco que podamos aadir a una
orden activa. Tienen cdigo de aplicacin especco que hay que aadir en el mtodo
execute().
Sin embargo, los forwarding commands actan en cierta forma como si se tratara
de punteros a funcin. El Invoker invoca el mtodo execute() del objeto orden y
ste a su vez ejecuta un mtodo del objeto Receiver al que est asociado. En [9] se
describe una tcnica interesante para este n, los generalized functors o adaptadores
polimrcos para objetos funcin. Se trata de una plantilla que encapsula cualquier
objeto, cualquier mtodo de ese objeto, y cualquier conjunto de argumentos para dicho
mtodo. Su ejecucin se traduce en la invocacin del mtodo sobre el objeto con
los argumentos almacenados. Este tipo de functors permiten reducir sensiblemente
el trabajo que implicara una jerarqua de rdenes concretas. Boost implementa una
tcnica similar en la plantilla function, que ha sido incorporada al nuevo estndar
de C++ (en la cabecera functional). Por ejemplo:
Listado 21.38: Ejemplo de uso de generalized functors.
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 <functional>
using namespace std;
int f1(const char* s) { return 0; }
struct f2 {
int operator() (const char* s) { return 0; }
};
struct A {
int fa(const char* s) { return 0; }
};
int
main()
{
function<int (const char*)> f;
f = f1; f("test1");
f = f2(); f("test2");
A a;
auto f3 = bind1st(mem_fun(&A::fa), &a);
f = f3; f("test3");
[557]
Consideraciones
El patrn command desacopla el objeto que invoca la operacin del objeto que
sabe cmo se realiza.
Al contrario que la invocacin directa, las rdenes son objetos normales. Pueden
ser manipulados y extendidos como cualquier otro objeto.
Las rdenes pueden ser agregadas utilizando el patrn composite.
Las rdenes pueden incluir transacciones para garantizar la consistencia sin ningn tipo de precaucin adicional por parte del cliente. Es el objeto Invoker el
que debe reintentar la ejecucin de rdenes que han abortado por un interbloqueo.
Si las rdenes a realizar consisten en invocar directamente un mtodo o una funcin se puede utilizar la tcnica de generalized functors para reducir el cdigo
necesario sin necesidad de implementar una jerarqua de rdenes.
21.2.3.
Este patrn fue inicialmente descrito y bautizado por James O. Coplien en [19].
Se trata de un patrn que ya se utilizaba aos antes, desde los primeros tiempos de las
plantillas de C++.
Problema
El patrn CRTP (Curiously Recurring Template Pattern) pretende extraer funcionalidad comn a varias clases, pero que requieren especializacin parcial para cada
una de ellas.
Solucin
La clase derivada hereda de una plantilla instanciada para ella misma. La clase
base cuenta en su implementacin con un tipo que deriva de ella misma. Por tanto la
propia clase base puede llamar a funciones especializadas en la clase derivada.
Implementacin
Se han propuesto multitud de casos donde puede aplicarse este patrn. Nosotros
destacaremos en primer lugar su uso para implementar visitantes alternativos a los ya
vistos en el mdulo 1.
Recordaremos brevemente la estructura del patrn visitante tal y como se cont en
el mdulo 1. Examinando la gura 21.15 podemos ver que:
C21
[558]
La clase base Visitor (y por tanto todas sus clases derivadas) es tremendamente dependiente de la jerarqua de objetos visitables de la izquierda. Si
se implementa un nuevo tipo de elemento ElementC (una nueva subclase de
Element) tendremos que aadir un nuevo mtodo visitElementB() en
la clase Visitor y con ello tendremos que reescribir todos y cada uno de
las subclases de Visitor. Cada clase visitable tiene un mtodo especco de
visita.
La jerarqua de elementos visitables no puede ser una estructura arbitraria, debe
estar compuesta por subclases de la clase Element e implementar el mtodo
accept().
Si se requiere cambiar la estrategia de visita. Por ejemplo, unicar el mtodo
de visita de dos tipos de elementos, es preciso cambiar la jerarqua de objetos
visitables.
El orden de visita de los elementos agregados est marcado por la implementacin concreta de las funciones accept() o visitX(). O bien se introduce
el orden de recorrido en los mtodos accept() de forma que no es fcil cambiarlo, o bien se programa a medida en los mtodos visitX() concretos. No
es fcil denir un orden de recorrido de elementos (en orden, en preorden, en
postorden) comn para todos las subclases de Visitor.
En general, se considera que el patrn visitor introduce un excesivo acoplamiento
en el cdigo y resulta tremendamente invasivo. Sin embargo, el patrn CRTP permite
aliviar gran parte de los problemas.
La jerarqua de visitables implementa el mtodo accept() exclusivamente para
que puedan elegir el mtodo visit() correcto de la clase derivada de Visitor.
Por eso se le llama tambin despachado doble. El despachado de la funcin virtual
accept() selecciona la subclase de Element concreta y a su vez ese elemento
concreto desencadena el despachado de visitX() que selecciona la subclase de
Visitor concreta. El segundo despachado es esencial para cualquier recorrido. Sin
embargo el primer despachado no siempre es necesario si conocemos de alguna manera el tipo a visitar. Por ejemplo, en el ejemplo del patrn visitor mostrado en el mdulo
1 el tipo de objetos es completamente jo. Sabemos que hay un objeto Scene que
contiene un nmero variable de objetos ObjectScene. Otra forma de realizar este
primer despachado podra ser utilizando RTTI (Run Time Type Information) u otro
mecanismo de introspeccin.
[559]
En este caso en que no sea necesario el primer despachado virtual se puede lograr
de una manera mucho ms eciente sin ni siquiera usar funciones virtuales, gracias al
patrn CRTP. Por ejemplo, el mismo ejemplo del mdulo 1 quedara as:
Listado 21.40: Visitante genrico usando el patrn CRTP.
struct ObjectScene {
string name;
Point position;
int weight;
};
struct Scene {
template <typename Derived> friend class Visitor;
string name;
vector<ObjectScene> objects;
};
template <typename Derived>
class Visitor {
public:
void traverseObject(ObjectScene* o) {
getDerived().visitObject(o);
}
void traverseScene(Scene* s) {
getDerived().visitScene(s);
for (auto o : s->objects)
traverseObject(o);
}
void visitObject(ObjectScene* o) {}
void visitScene(Scene* s) {}
private:
Derived& getDerived() {
return *static_cast<Derived*>(this);
}
};
class NameVisitor : public Visitor<NameVisitor> {
vector<string> _names;
public:
void visitObject(ObjectScene* o) {
_names.push_back(o->name);
}
void visitScene(Scene* s) {
cout << "The scene " << s->name << " has the following
objects:"
<< endl;
for (auto n : _names) cout << n << endl;
}
};
40
41
42
43
44
45 class BombVisitor : public Visitor<BombVisitor> {
46
Bomb _bomb;
47 public:
48
BombVisitor(const Bomb& bomb) : _bomb(bomb) {}
49
void visitObject(ObjectScene* o) {
50
Point new_pos = calculateNewPosition(o->position,
51
o->weight,
52
_bomb.intensity);
53
o->position = new_pos;
54
}
55 };
C21
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
[560]
1
2
3
4
5
6
7
Derived>& o2)
Y con ello podemos denir todos los operadores de golpe sin ms que denir
operator <.
Listado 21.43: Ejemplo de mixin con CRTP para implementacin automtica de operadores.
1 class Person : public Comparisons<Person> {
2 public:
3
Person(string name_, unsigned age_)
4
: name(name_), age(age_) {}
5
6
friend bool operator<(const Person& p1, const Person& p2);
7 private:
8
string name;
8 http://eli.thegreenplace.net/2011/05/17/the-curiously-recurring-template-pattern-in-c/
[561]
9
unsigned age;
10 };
11
12 bool operator<(const Person& p1, const Person& p2) {
13
return p1.age < p2.age;
14 }
Consideraciones
La tcnica que explota el patrn CRTP es denominada a veces como polimorsmo esttico, por contraposicin al dinmico de las funciones virtuales. La clase base
utiliza la implementacin correcta de los mtodos redenidos en las clases derivadas
porque se le pasa como parmetro de plantilla. Esto es una ventaja y un inconveniente
a la vez.
Por un lado la utilizacin de funciones no virtuales elimina las indirecciones y
permite que sea lo ms eciente posible. Pero por otro lado no puede inferir el tipo de
un objeto a travs de un puntero a la clase base. Por ejemplo, si en el caso del visitante
hubiera varios tipos derivados de ObjectScene y la clase Scene almacenara punteros a ObjectScene, el mtodo traverseObject() no podra determinar qu
funcin de visita debe invocar. La solucin estndar en este caso sera emplear RTTI
(run-time type information) para determinar el tipo de objeto en tiempo de ejecucin,
pero eso es mucho menos eciente que las funciones virtuales.
Listado 21.44: Uso de RTTI para especializar la visita de objetos.
21.2.4.
Reactor
C21
1 void traverseObject(ObjectScene* o) {
2
Character* c = dynamic_cast<Character*>(o);
3
if (c) {
4
getDerived().visitCharacter(c);
5
return;
6
}
7
Weapon* w = dynamic_cast<Weapon*>(o);
8
if (w) {
9
getDerived().visitCharacter(w);
10
return;
11
}
12 }
[562]
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 denen una serie de actores con las siguientes responsabilidades (vase gura 21.16):
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 reere 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 chero.
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 desmultiplexacin 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().
En general, el comportamiento sera el siguiente:
1. Los manejadores se registran utilizando el mtodo regHandler() del Reactor. De esta forma, el Reactor puede congurarse para esperar los eventos del
recurso que el manejador espera. El manejador puede dejar de recibir noticaciones con unregHandler().
2. A continuacin, el Reactor entra en el bucle innito (loop()), en el que se
espera la ocurrencia de eventos.
[563]
3. Utilizando alguna llamada al sistema, como puede ser select(), el Reactor
espera a que se produzca algn evento sobre los recursos monitorizados.
4. Cuando ocurre, busca los manejadores asociados a ese recurso y les invoca el
mtodo handle() con el evento que ha ocurrido como parmetro.
5. El manejador recibe la invocacin y ejecuta todo el cdigo asociado al evento.
Ntese que aunque los eventos ocurran concurrentemente el Reactor serializa las
llamadas a los manejadores. Por lo tanto, la ejecucin de los manejadores de eventos
ocurre de forma secuencial.
Implementacin
Desde el punto de vista de implementacin un Reactor se comporta como un envoltorio orientado a objetos de los servicios de demultiplexin de eventos del sistema operativo. Ofrece una interfaz homognea para llamadas al sistema tales como
select(), poll(), o los IO Completion Ports de Windows.
Una implementacin exible de este patrn es la incluida en ACE (Adaptive Communications Environment) (Adaptive Communications Environment9 ). ste sera un
ejemplo mnimo con un manejador de eventos de teclado:
Listado 21.45: Ejemplo de uso de patrn reactor.
#include <ace/Reactor.h>
class MyEvHandler : public ACE_Event_Handler {
virtual int handle_input (ACE_HANDLE h) {
char buf[256];
int n = ::read(h, buf, sizeof buf);
if (n <= 0) return -1;
// procesar buf ...
return 0;
}
};
int main (int argc, const char *argv[]) {
ACE_Reactor reactor;
MyEvHandler h;
reactor.register_handler(ACE_STDIN,
&h,
ACE_Event_Handler::READ_MASK);
for(;;)
reactor.handle_events();
}
return 0;
Para mezclar el bucle de eventos de Ogre con cualquier implementacin del patrn reactor basta eliminar la llamada a
Root::startRendering() e incluir dentro del bucle del Reactor
una llamada a Root::renderOneFrame().
9 http://www.cs.wustl.edu/~schmidt/ACE.html
C21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[564]
Consideraciones
Al utilizar un Reactor, se deben tener las siguientes consideraciones:
1. Los manejadores de eventos no pueden consumir mucho tiempo. Si lo hacen,
pueden provocar un efecto convoy y, dependiendo de la frecuencia de los eventos, pueden hacer que el sistema sea inoperable. En general, cuanto mayor sea
la frecuencia en que ocurren los eventos, menos tiempo deben consumir los
manejadores.
2. Existen implementaciones de Reactors que permiten una desmultiplexacin concurrente.
3. Desde un punto de vista general, el patrn Observer tiene un comportamiento
muy parecido. Sin embargo, el Reactor est pensado para las relaciones 1 a 1 y
no 1 a n como en el caso del Observer visto en el mdulo 1.
21.2.5.
Acceptor/Connector
[565]
Solucin
El funcionamiento es como sigue:
C21
Un Acceptor es una factora que implementa el rol pasivo para establecer conexiones. Ante una conexin crea e inicializa un Transport Handle y un
Service Handler asociados. En su inicializacin, un Acceptor se asocia
a una direccin de transporte (e.g. direccin IP y puerto TCP), y se congura para aceptar conexiones en modo pasivo. Cuando llega una solicitud de conexin
realiza tres pasos:
[566]
#include <ace/SOCK_Acceptor.h>
#include <ace/Acceptor.h>
#include <ace/Svc_Handler.h>
class MySvcHandler : public ACE_Svc_Handler<ACE_SOCK_STREAM,
ACE_MT_SYNCH> {
virtual int handle_input (ACE_HANDLE) {
char buf[256];
int n = peer().recv(buf, sizeof buf);
if (n <= 0) return -1;
// procesar buf ...
return 0;
}
};
6
7
8
9
10
11
12
13
14
15 typedef ACE_Acceptor <MySvcHandler, ACE_SOCK_ACCEPTOR> MyAcceptor;
16
17 int main (int argc, const char *argv[]) {
18
ACE_Reactor reactor;
19
MyAcceptor acceptor;
20
21
acceptor.open(ACE_INET_Addr(9999), &reactor);
22
for(;;) reactor.handle_events();
23
return 0;
24 }
[567]
Especializamos la plantilla del Acceptor con un Svc_Handler que tiene la lgica de intercambio de mensajes. Al instanciar el Acceptor le pasamos un Reactor
para que automticamente registre los nuevos Svc_Handler que crea en las nuevas
conexiones.
El lado del cliente es muy similar, salvo que en este caso utilizamos un Connector.
Listado 21.47: Ejemplo de uso de patrn acceptor-connector (cliente).
1
2
3
4
5
#include <ace/SOCK_Connector.h>
#include <ace/Connector.h>
#include <ace/Svc_Handler.h>
class MySvcHandler : public ACE_Svc_Handler<ACE_SOCK_STREAM,
ACE_MT_SYNCH> {
virtual int handle_output (ACE_HANDLE) {
char buf[]="Hello, World!\n";
int n = peer().send(buf, sizeof buf);
if (n <= 0) return -1;
return 0;
}
};
6
7
8
9
10
11
12
13
14 typedef ACE_Connector <MySvcHandler, ACE_SOCK_CONNECTOR>
MyConnector;
15
16 int main (int argc, const char *argv[]) {
17
ACE_Reactor reactor;
18
MyConnector connector;
19
MySvcHandler* psvc = 0;
20
21
int n = connector.connect(psvc, ACE_INET_Addr(9999,"127.0.0.1")
);
if (n < 0) return 1;
reactor.register_handler(psvc, ACE_Event_Handler::WRITE_MASK);
for(;;)
reactor.handle_events();
return 0;
C21
22
23
24
25
26
27
28
29 }
[568]
21.3.
Programacin genrica
La programacin genrica es un paradigma de programacin que trata de conseguir un mayor grado de reutilizacin tanto de las estructuras de datos como de los
algoritmos, evitando as la duplicidad de cdigo. Para lograrlo, los algoritmos deben
escribirse evitando asociar los detalles a tipos de datos concretos. Por ejemplo, en
un algoritmo de ordenacin, la operacin que compara dos elementos cualesquiera se
delega a una entidad ajena al algoritmo: un operador de comparacin.
Hoy en da, prcticamente todos los lenguajes de programacin importantes disponen o han adoptado caractersticas de programacin genrica (tal como los llamados
genricos en Java o C#).
El diseo de la librera STL pretende proporcionar herramientas bsicas de programacin genrica. No es casualidad que la creacin de STL y las ideas tras el paradigma de la programacin genrica fueran desarrolladas por los mismos autores,
especialmente Alexander Stepanov y David Musser [65]. Y de ah el inters por separar las estructuras de datos (los contenedores) de los algoritmos. Como veremos,
los otros dos componentes de la STL (iteradores y functors) sirven tambin al mismo propsito: posibilitan la interaccin entre contenedores y algoritmos, a la vez que
permiten un acoplamiento mnimo.
Es interesante indicar que la disociacin entre los datos y los algoritmos que los
manejan contradice en cierta medida los principios de la programacin orientada a objetos. En la POO las operaciones relativas a un tipo de dato concreto se ofrecen como
mtodos de dicha clase. El polimorsmo por herencia10 permite en la prctica utilizar
un algoritmo denido como un mtodo de la superclase con instancias de sus subclases. Sin embargo, esto no se considera programacin genrica pues la implementacin
del algoritmo normalmente depende al menos de la superclase de la jerarqua.
En STL los algoritmos estn implementados normalmente como funciones (no
mtodos) y por supuesto no tienen estado, algo que por denicin es ajeno a la POO.
A pesar de ello, en el diseo de la librera estn muy presentes los principios de orientacin a objetos.
21.3.1.
Algoritmos
[569]
pues, el algoritmo equal() espera que los elementos soporten en modelo EqualityComparable, que implica que tienen sobrecargado el mtodo operator==(),
adems de cumplir ste ciertas condiciones como reexibidad, simetra, transitividad,
etc.
algoritmos escalares
return retval;
void test_my_count1() {
const int size = 5;
const int value = 1;
int numbers[] = {1, 2, 3, 1, 2};
}
Recuerda, en las funciones, aquellos parmetros que no impliquen copia (puntero o referencia) deberan ser constantes si la funcin efectivamente no va a
modicarlos.
C21
[570]
return retval;
void test_my_count3() {
const int size = 5;
const int value = 1;
int numbers[] = {1, 2, 3, 1, 2};
}
return retval;
void test_my_count4_numbers() {
const int size = 5;
const int value = 1;
int numbers[] = {1, 2, 3, 1, 2};
vector<int> numbers_vector(numbers, numbers + size);
assert(my_count4(numbers, numbers+size, value) == 2);
assert(my_count4(numbers_vector.begin(), numbers_vector.end(),
value) == 2);
[571]
20 }
21
22 void test_my_count4_letters() {
23
const int size = 5;
24
const int value = a;
25
char letters[] = {a, b, c, a, b};
26
vector<char> letters_vector(letters, letters + size);
27
28
assert(my_count4(letters, letters+size, value) == 2);
29
assert(my_count4(letters_vector.begin(), letters_vector.end(),
30
value) == 2);
31 }
void test_count_letters() {
const int size = 5;
const int value = a;
char letters[] = {a, b, c, a, b};
vector<char> letters_vector(letters, letters + size);
21.3.2.
Predicados
Por ese motivo, muchos algoritmos de la STL tienen una versin alternativa que
permite especicar un parmetro adicional llamado predicado. El algoritmo invocar
el predicado para averiguar si se cumple la condicin indicada por el programador y
as determinar cmo debe proceder con cada elemento de la secuencia.
En C/C++, para que una funcin pueda invocar a otra (en este caso, el algoritmo
al predicado) se le debe pasar como parmetro un puntero a funcin.
11 Para
C21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[572]
Veamos la denicin de un predicado (not_equal_2) que, como habr imaginado, ser cierto para valores distintos a 2:
Listado 21.53: Predicado not_equal_2()
1
2
3
4
5
6
7
8
9
10
bool not_equal_2(int n) {
return n != 2;
}
void test_not_equal_2() {
const int size = 5;
int numbers[] = {1, 2, 3, 1, 2};
}
Igual que con cualquier otro tipo de dato, cuando se pasa un puntero a funcin
como argumento, el parmetro de la funcin que lo acepta debe estar declarado con
ese mismo tipo. Concretamente el tipo del predicado not_equal_2 sera algo como:
Listado 21.54: Tipo para un predicado que acepta un argumento entero
1 bool (*)(int);
21.3.3.
Functors
Existe sin embargo una solucin elegante para conseguir predicados congurables. Consiste es declarar una clase que sobrecargue el operador de invocacin
mtodo operator()() que permite utilizar las instancias de esa clase como si
fueran funciones. Las clases que permiten este comportamiento se denominan functors13 . Veamos como implementar un predicado not_equal() como un functor:
Listado 21.55: Predicado not_equal() para enteros (como functor)
1 class not_equal {
2
const int _ref;
3
4 public:
5
not_equal(int ref) : _ref(ref) {}
6
7
bool operator()(int value) {
8
return value != _ref;
9
}
10 };
12 En
13 functor
[573]
Y dos pruebas que demuestran su uso:
Listado 21.56: Ejemplos de uso de not_equal()
1
2
3
4
5
6
7
8
9
10
11
12
13
void test_not_equal_functor() {
not_equal not_equal_2(2);
assert(not_equal_2(0));
assert(not not_equal_2(2));
void test_not_equal_count_if() {
const int size = 5;
int numbers[] = {1, 2, 3, 1, 2};
}
Pero los predicados no son la nica utilidad interesante de los functors. Los predicados son una particularizacin de las funciones (u operadores). Los operadores
pueden devolver cualquier tipo. no slo booleanos.
C21
[574]
Como ejemplo, el siguiente listado multiplica los elementos del array numbers:
Listado 21.58: accumulate() multiplicando los elementos de una secuencia de enteros
1 void test_accumulate_multiplies() {
2
int numbers[] = {1, 2, 3, 4};
3
const int size = sizeof(numbers) / sizeof(int);
4
5
int result = accumulate(numbers, numbers+size,
6
1, multiplies<int>());
7
assert(result == 24);
8 }
21.3.4.
Adaptadores
Es habitual que la operacin que nos gustara que ejecute el algoritmo sea un
mtodo (una funcin miembro) en lugar de una funcin estndar. Si tratamos de pasar
al algoritmo un puntero a mtodo no funcionar, porque el algoritmo no le pasar el
parmetro implcito this que todo mtodo necesita.
Una posible solucin sera escribir un functor que invoque el mtodo deseado,
como se muestra en el siguiente listado.
Listado 21.59: Adaptador manual para un mtodo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Enemy {
public:
bool is_alive(void) const {
return true;
}
};
class enemy_alive {
public:
bool operator()(Enemy enemy) {
return enemy.is_alive();
}
};
void test_my_adapter() {
vector<Enemy> enemies(2);
assert(count_if(enemies.begin(), enemies.end(),
enemy_alive()) == 2);
mem_fun()
Listado 21.60: Uso del adaptador mem_fun_ref()
1 void test_mem_fun_ref() {
2
vector<Enemy> enemies(2);
3
[575]
4
assert(count_if(enemies.begin(), enemies.end(),
5
mem_fun_ref(&Enemy::is_alive)) == 2);
6 }
assert(count_if(numbers, numbers+size,
bind2nd(ptr_fun(not_equal), 2)) == 3);
Ntese que bind2nd() espera un functor como primer parmetro. Como lo que
tenemos es una funcin normal es necesario utilizar otro adaptador llamado ptr_fun(),
que como su nombre indica adapta un puntero a funcin a functor.
bind2nd() pasa su parmetro adicional (el 2 en este caso) como segundo parmetro en la llamada a la funcin not_equal(), es decir, la primera llamada para
la secuencia del ejemplo ser not_equal(1, 2). El primer argumento (el 1) es
el primer elemento obtenido de la secuencia. El adaptador bind1st() los pasa en
orden inverso, es decir, pasa el valor extra como primer parmetro y el elemento de la
secuencia en segunda posicin.
not1() devuelve un predicado que es la negacin lgica del predicado unario al que
se aplique.
C21
compose1() devuelve un operador resultado de componer las dos funciones unarias que se le pasan como parmetros. Es decir, dadas las funciones f (x) y g(x)
devuelve una funcin f (g(x)).
compose2() devuelve un operador resultado de componer una funcin binaria y
dos funciones unarias que se le pasan como parmetro del siguiente modo. Dadas las funciones f (x, y), g1 (x) y g2 (x) devuelve una funcin h(x) = f (g1 (x), g2 (x)).
[576]
21.3.5.
Algoritmos idempotentes
class Weapon {
public:
void reload() { /* some code */ }
};
void test_for_each() {
vector<Weapon> weapons(5);
for_each(weapons.begin(), weapons.end(),
mem_fun_ref(&Weapon::reload));
}
find() / find_if()
Devuelve un iterador al primer elemento del rango que coincide con el indicado
(si se usa find()) o que cumple el predicado (si se usa find_if()). Devuelve el
iterador end si no encuentra ninguna coincidencia. Un ejemplo en el que se busca el
primer entero mayor que 6 que haya en el rango:
Listado 21.63: Ejemplo de find_if()
1 void test_find_if() {
2
const int size = 5;
3
const int value = 1;
4
int numbers[] = {2, 7, 12, 9, 4};
5
6
assert(find_if(numbers, numbers + size,
7
bind2nd(greater<int>(), 6)) == numbers+1);
8 }
Se utiliza greater, un predicado binario que se cumple cuando su primer parmetro (el elemento del rango) es mayor que el segundo (el 6 que se pasa bind2nd()).
El resultado del algoritmo es un iterador al segundo elemento (el 7) que corresponde con numbers+1. Hay algunos otros functors predenidos para comparacin:
equal_to(), not_equal_to()14 , less(), less_equal() y greater_equal().
14 Equivalente
[577]
count() / count_if()
Como ya hemos visto en ejemplos anteriores count() devuelve la cantidad de
elementos del rango igual al dado, o que cumple el predicado, si se usa la modalidad
count_if().
mismatch()
Dados dos rangos, devuelve un par de iteradores a los elementos de cada rango en
el que las secuencias dieren. Veamos el siguiente ejemplo extrado de la documentacin de SGI15 :
Listado 21.64: Ejemplo de uso de mismatch()
1 void test_mismatch() {
2
int A1[] = { 3, 1, 4, 1, 5, 9, 3};
3
int A2[] = { 3, 1, 4, 2, 8, 5, 7};
4
const int size = sizeof(A1) / sizeof(int);
5
6
pair<int*, int*> result = mismatch(A1, A1 + size, A2);
7
assert(result.first == A1 + 3);
8
assert((*result.first) == 1 and (*result.second) == 2);
9 }
Indica si los rangos indicados son iguales. Por defecto utiliza el operator==(),
pero opcionalmente es posible indicar un predicado binario como cuarto parmetro
para determinar en qu consiste la igualdad. Veamos un ejemplo en el que se comparan dos listas de guras que se considerarn iguales simplemente porque coincida
su color:
Listado 21.65: Ejemplo de uso de equal()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 http://www.sgi.com/tech/stl/mismatch.html
C21
equal()
[578]
15
Shape shapes1[size], shapes2[size];
16
shapes2[3].color = RED;
17
18
assert(equal(shapes1, shapes1+size, shapes2,
19
mem_fun_ref(&Shape::cmp)) == false);
20 }
search()
Localiza la posicin del segundo rango en el primero. Devuelve un iterador al primer elemento. Opcionalmente acepta un predicado binario para especicar la igualdad
entre dos elementos. Veamos este ejemplo extrado de la documentacin de SGI.
Listado 21.66: Ejemplo de uso de search()
1 void test_search() {
2
const char s1[] = "Hello, world!";
3
const char s2[] = "world";
4
const int n1 = strlen(s1);
5
const int n2 = strlen(s2);
6
7
const char* p = search(s1, s1 + n1, s2, s2 + n2);
8
assert(p == s1 + 7);
9 }
21.3.6.
Algoritmos de transformacin
Normalmente, en los algoritmos de transformacin (mutating algorithms) se distingue entre el rango o secuencia de entrada y la de salida, ya que su operacin implica
algn tipo de modicacin (insercin, eliminacin, cambio, etc.) sobre los elementos
de la secuencia de salida.
Es importante recordar que las secuencias de salida que se utilizan en los
algoritmos de transformacin deben disponer de memoria suciente para los
datos que recibirn u obtendremos comportamientos errticos aleatorios y
errores de acceso a memoria en tiempo de ejecucin (SEGFAULT).
copy()
Copia los elementos de un rango en otro. No debera utilizarse para copiar una
secuencia completa en otra ya que el operador de asignacin que tienen todos los
contenedores resulta ms eciente. S resulta interesante para copiar fragmentos de
secuencias. Veamos un uso interesante de copy() para enviar a un ujo (en este caso
cout) el contenido de una secuencia.
[579]
Listado 21.67: Ejemplo de uso de copy()
1 int main() {
2
int values[] = {1, 2, 3, 4, 5};
3
const int size = sizeof(values) / sizeof(int);
4
5
copy(values+2, values+size,
6
ostream_iterator<int>(cout, ", "));
7
cout << endl;
8 }
C21
[580]
replace() / replace_if()
Dado un rango, un valor antiguo y un valor nuevo, substituye todas las ocurrencias
del valor antiguo por el nuevo en el rango. La versin replace_if() substituye
los valores que cumplan el predicado unario especicado por el valor nuevo. Ambos
utilizan un nica secuencia, es decir, hacen la substitucin in situ.
Existen variantes llamadas replace_copy() y replace_copy_if() respectivamente en la que se copian los elementos del rango de entrada al de salida a la
vez que se hace la substitucin. En este caso la secuencia original no cambia.
fill()
Dado un rango y un valor, copia dicho valor en todo el rango:
Listado 21.69: Ejemplo de uso de fill()
1 void test_fill() {
2
vector<float> v(10);
3
assert(count(v.begin(), v.end(), 0));
4
5
fill(v.begin(), v.end(), 2);
6
assert(count(v.begin(), v.end(), 2) == 10);
7 }
class next {
int _last;
public:
next(int init) : _last(init) {}
int operator()() {
return _last++;
}
};
void test_generate() {
vector<int> v(10);
generate(v.begin(), v.end(), next(10));
assert(v[9] == 19);
}
[582]
rotate()
Rota los elementos del rango especicado por 3 iteradores, que indican el inicio,
el punto medio y el nal del rango. Existe una modalidad rotate_copy(), que
como siempre aplica el resultado a un iterador en lugar de modicar el original.
Algoritmos aleatorios
Hay tres algoritmos que tienen que ver con operaciones aleatorias sobre una secuencia:
random_shuffle() reordena de forma aleatoria los elementos del rango.
random_sample() elige aleatoriamente elementos de la secuencia de entrada y
los copia en la secuencia de salida. Es interesante destacar que este algoritmo
requiere 4 iteradores ya que se puede crear una secuencia de salida de tamao
arbitrario, siempre que sea menor o igual que la secuencia de entrada.
random_shuffle_n() realiza la misma operacin que random_sample() salvo que la cantidad de elementos a generar se especica explcitamente en lugar
de usar un cuarto iterador. Eso permite utilizarlo con un iterador de insercin.
Los tres aceptan opcionalmente una funcin que genere nmeros aleatorios.
partition()
Dada una secuencia y un predicado, el algoritmo reordena los elementos de modo
que los que satisfacen el predicado aparecen primero y los que lo incumplen despus.
Devuelve un iterador al primer elemento que incumple el predicado.
La modalidad stable_partition() preserva el orden de los elementos en
cada parte respecto al orden que tenan en la secuencia original.
21.3.7.
Algoritmos de ordenacin
[583]
Listado 21.73: Ejemplo de uso de sort()
1
2
3
4
5
6
7
8
9
10
11
12
13
C21
[584]
5
6
7
8
9
10
11
12
13
14
15
16
17
void test_bounds() {
assert(lower_bound(numbers, numbers+size, 6) == numbers+2);
assert(upper_bound(numbers, numbers+size, 8) == numbers+4);
}
void test_equal_range() {
pair<int*, int*> bounds = equal_range(numbers, numbers+size, 7);
assert(bounds.first == numbers+2 and bounds.second == numbers+4);
}
merge() combina dos secuencias, dadas por dos pares de iteradores, y crea una
tercera secuencia que incluye los elementos de ambas, manteniendo el orden.
Mnimo y mximo
Los algoritmos min_element() y max_element() permiten obtener respectivamente el elemento mnimo y mximo del rango especicado. Veamos un ejemplo:
Listado 21.75: Ejemplo de uso de max_element() y min_element()
1
2
3
4
5
6
7
8
9
10
11
12
21.3.8.
Algoritmos numricos
accumulate()
Aplica un operador (la suma si no se especica otro) sobre el rango especicado
por dos iteradores. Debe indicarse tambin un valor inicial ya que el algoritmo opera
sobre un valor acumulado (de ah su nombre) y un elemento extrado de la secuencia.
El listado 21.58 muestra un ejemplo de uso.
partial_sum()
Calcula la suma parcial para cada elemento de una secuencia y lo almacena
sobre un iterador de salida:
Listado 21.76: Ejemplo de uso de partial_sum()
1 void test_partial_sum() {
2
const int size = 5;
[585]
3
4
5
6
7
8
9
10 }
vector<int> values(size);
fill(values.begin(), values.end(), 1);
partial_sum(values.begin(), values.end(), values.begin());
int expected[size] = {1, 2, 3, 4, 5};
assert(equal(values.begin(), values.end(), expected));
adjacent_difference()
Calcula las diferencias entre elementos consecutivos de la secuencia de entrada y
los escribe sobre el iterador de salida:
Listado 21.77: Ejemplo de uso de adjacent_difference()
1 void test_adjacent_difference() {
2
int values[] = {1, 3, 0, 10, 15};
3
const int size = sizeof(values) / sizeof(int);
4
int result[size];
5
6
adjacent_difference(values, values+size, result);
7
8
int expected[size] = {1, 2, -3, 10, 5};
9
assert(equal(result, result+size, expected));
10 }
21.3.9.
Veamos un programa concreto que ilustra como sacar partido de las algoritmos
genricos. Se trata del tpico inventario de armas habitual en cualquier videojuego
tipo shooter.
C21
[586]
21
}
22
23
void add_ammo(int amount) {
24
ammo = min(ammo + amount, max_ammo());
25
}
26
27
void add_ammo(Weapon* other) {
28
add_ammo(other->ammo);
29
}
30
31
int less_powerful_than(Weapon* other) const {
32
return power() < other->power();
33
}
34
35
bool same_weapon_as(Weapon* other) {
36
return typeid(*this) == typeid(*other);
37
}
38 };
Note cmo los mtodos shoot(), is_empty() y get_ammo() son autoexplicativos. El mtodo add_ammo() est sobrecargado. La primera versin (lnea
23) aade al arma la cantidad especicada de balas respetando el lmite. Para esto se
utiliza el algoritmo min().
El mtodo less_powerful_than() compara esta instancia de arma con otra
para decidir cul es la ms potente, y por ltimo, el mtodo same_weapon_as()
indica si el arma es del mismo tipo utilizando RTTI.
El siguiente listado muestra tres especializaciones de la clase Weapon que nicamente especializan los mtodos privados power() y max_ammo() para cada uno
de los tipos Pistol, Shotgun y RPG.
Listado 21.79: Especializaciones de Weapon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[587]
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
push_back(weapon);
WeaponVector weapons_with_ammo(void) {
WeaponVector retval;
remove_copy_if(begin(), end(), back_inserter(retval),
mem_fun(&Weapon::is_empty));
if (retval.begin() == retval.end())
throw Inventory::WeaponNotFound();
}
return retval;
Weapon* more_powerful_weapon(void) {
WeaponVector weapons = weapons_with_ammo();
sort(weapons.begin(), weapons.end(),
mem_fun(&Weapon::less_powerful_than));
}
return *(weapons.end()-1);
La lnea 3 dene el tipo WeaponIter como alias del tipo del iterador para
recorrer el contenedor.
En la lnea 4, la clase WeaponNotFound se utiliza como excepcin en las
bsquedas de armas, como veremos a continuacin.
El mtodo add() se utiliza para aadir un nuevo arma al inventario, pero contempla especcamente el caso habitual en los shotters en el que coger un arma
que ya tiene el jugador implica nicamente coger su municin y desechar el arma.
Para ello, utiliza el algoritmo find_if() para recorrer el propio contenedor especicando como predicado el mtodo Weapon::same_weapon_as(). Ntese el
uso de los adaptadores mem_fun() (por tratarse de un mtodo) y de bind2nd()
para pasar a dicho mtodo la instancia del arma a buscar. Si se encuentra un arma del
mismo tipo (lneas 1216) se aade su municin al arma existente usando el iterador
devuelto por find_if() y se elimina (lnea 14). En otro caso se aade la nueva arma
al inventario (lnea 18).
C21
[588]
Por otra parte, el mtodo more_powerful_weapon() (lneas 3441) implementa una funcionalidad tambin muy habitual en ese tipo de juegos: cambiar al arma
ms potente disponible. En este contexto, invoca weapons_with_ammo() (lneas
2232) para obtener las armas con municin. Utiliza el algoritmo remove_copy_if()
para crear un vector de punteros (mediante la funcin back_inserter()), evitando copiar las vacas (lnea 26).
Ordena el vector resultante usando sort() y utilizando como predicado el mtodo less_powerful_than() que vimos antes. Por ltimo, el mtodo retorna un
puntero al ltimo arma (lnea 40). Ntese que el * en esa lnea es la de-referencia
del iterador (que apunta a un puntero).
Para acabar, se muestra el destructor de la clase, que se encarga de liberar los
punteros que almacena:
Listado 21.81: Inventario de armas: Destructor
1 template<class T>
2 T* deleter(T* x) {
3
delete x;
4
return 0;
5 }
6
sort(weapons.begin(), weapons.end(),
7
mem_fun(&Weapon::less_powerful_than));
8
9
return *(weapons.end()-1);
10
}
Aqu se utiliza el functor (deleter) con el algoritmo transform() para liberar cada puntero. La razn de usar transform() en lugar de for_each() es
eliminar las direcciones de los punteros que dejan de ser vlidos en el contenedor.
Despus se borra todo el contenedor usando su mtodo clear().
21.4.
En esta seccin veremos cmo explotar el potencial de la librera STL ms all del
mero uso de sus contenedores y algoritmos.
21.4.1.
Eciencia
La eciencia es sin duda alguna uno de los objetivos principales de la librera STL.
Esto es as hasta el punto de que se obvian muchas comprobaciones que haran su uso
ms seguro y productivo. El principio de diseo aplicado aqu es:
Es factible construir decoradores que aadan comprobaciones adicionales a la versin eciente. Sin embargo no es posible construir una versin
eciente a partir de una segura que realiza dichas comprobaciones.
Algunas de estas comprobaciones incluyen la dereferencia de iteradores nulos,
invalidados o fuera de los lmites del contenedor, como se muestra en el siguiente
listado.
Para subsanar esta situacin el programador puede optar entre utilizar una implementacin que incorpore medidas de seguridad con la consiguiente reduccin de
eciencia o bien especializar los contenedores en clases propias y controlar especcamente las operaciones susceptibles de ocasionar problemas.
Principio de Pareto
El principio de Pareto tambin es
aplicable a la ejecucin de un programa. Estadsticamente el 80 %
del tiempo de ejecucin de un
programa es debido nicamente al
20 % de su cdigo. Eso signica
que mejorando ese 20 % se pueden conseguir importantes mejoras.
Por ese motivo, la optimizacin del
programa (si se necesita) debera
ocurrir nicamente cuando se haya
identicado dicho cdigo por medio de herramientas de perlado y
un adecuado anlisis de los ujos
de ejecucin. Preocuparse por optimizar una funcin lenta que solo se
invoca en el arranque de un servidor
que se ejecuta durante das es perjudicial. Supone un gasto de recursos
y tiempo que probablemente producir cdigo menos legible y mantenible.
[589]
En cualquier caso el programador debera tener muy presente que este tipo de decisiones ad hoc (eliminar comprobaciones) forman parte de la fase de optimizacin y
slo deberan considerarse cuando se detecten problemas de rendimiento. En general,
tal como dice Ken Beck, La optimizacin prematura es un lastre. Es costosa (en
tiempo y recursos) y produce normalmente cdigo ms sucio, difcil de leer y mantener, y por tanto, de inferior calidad.
Listado 21.82: Situaciones no controladas en el uso de iteradores
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void test_lost_iterator() {
vector<int>::iterator it;
int i = *it; // probably a SEGFAULT
}
void test_invalidated_iterator() {
vector<int> v1;
v1.push_back(1);
vector<int>::iterator it = v1.begin();
v1.clear();
}
void test_outbound_iterator() {
vector<int> v1;
vector<int>::iterator it = v1.end();
}
Sin embargo, existen otro tipo de decisiones que el programador puede tomar
cuando utiliza la STL, que tienen un gran impacto en la eciencia del resultado y que
no afectan en absoluto a la legibilidad y mantenimiento del cdigo. Estas decisiones
tienen que ver con la eleccin del contenedor o algoritmo adecuado para cada problema concreto. Esto requiere conocer con cierto detalle el funcionamiento y diseo de
los mismos.
A continuacin se listan los aspectos ms relevantes que se deberan tener en cuenta al elegir un contenedor, considerando las operaciones que se realizarn sobre l:
Tamao medio del contenedor.
En general, la eciencia en cuanto a tiempo de acceso solo es signicativa para grandes cantidades de elementos. Para menos de 100 elementos (seguramente
muchos ms considerando las computadoras o consolas actuales) es muy probable que la diferencia entre un contenedor con tiempo de acceso lineal y uno
logartmico sea imperceptible. Si lo previsible es que el nmero de elementos
sea relativamente pequeo o no se conoce bien a priori, la opcin ms adecuada
es vector.
Insercin de elementos en los dos extremos de la secuencia.
Si necesita aadir al comienzo con cierta frecuencia (>10 %) debera elegir un
contenedor que implemente esta operacin de forma eciente como deque.
Insercin y borrado en posiciones intermedias.
El contenedor ms adecuado en este caso es list. Al estar implementado como
una lista doblemente enlazada, la operacin de insercin o borrado implica poco
ms que actualizar dos punteros.
C21
[590]
Otro aspecto a tener en cuenta es la distincin entre contenedores basados en bloques (como vector, deque o string) y los basados en nodos (como list, set,
map, etc.). Los contenedores basados en nodos almacenan cada elemento como unidades independientes y se relacionan con los dems a travs de punteros. Esto tiene
varias implicaciones interesantes:
Si se obtiene un iterador a un nodo, sigue siendo vlido durante toda la vida del
elemento. Sin embargo, en los basados en bloque los iteradores pueden quedar
invalidados si se realoja el contenedor.
Ocupan ms memoria por cada elemento almacenado, debido a que se requiere
informacin adicional para mantener la estructura: rbol o lista enlazada.
Elegir el algoritmo adecuado
Aunque los algoritmos de STL estn diseados para ser ecientes (el estndar incluso determina la complejidad ciclomtica mxima permitida) ciertas operaciones
sobre grandes colecciones de elementos pueden implicar tiempos de cmputo muy
considerables. Para reducir el nmero de operaciones a ejecutar es importante considerar los condicionantes especcos de cada problema.
Uno de los detalles ms simples a tener en cuenta es la forma en la que se especica la entrada al algoritmo. En la mayora de ellos la secuencia queda determinada
por el iterador de inicio y el de n. Lo interesante de esta interfaz es que darle al algoritmo parte del contenedor es tan sencillo como drselo completo. Se pueden dar
innumerables situaciones en las que es perfectamente vlido aplicar cualquiera de los
algoritmos genricos que hemos visto a una pequea parte del contenedor. Copiar,
buscar, reemplazar o borrar en los n primeros o ltimos elementos puede servir para
lograr el objetivo ahorrando muchas operaciones innecesarias.
Otra forma de ahorrar cmputo es utilizar algoritmos que hacen solo parte del
trabajo (pero suciente en muchos casos), en particular los de ordenacin y bsqueda
como partial_sort(), nth_element(), lower_bound(), etc.
Por ejemplo, una mejora bastante evidente que se puede hacer a nuestro inventario
de armas (ver listado 21.80) es cambiar el algoritmo sort() por max_element()
en el mtodo more_powerful_weapon().
Listado 21.83: Modicacin del inventario de armas
1
2
3
4
5
6
Weapon* more_powerful_weapon(void) {
WeaponVector weapons = weapons_with_ammo();
[591]
El libro Effective STL [63] de Scott Meyers explica muchas otras reglas concretas en las que el uso adecuado de STL puede aumentar notablemente la eciencia
del programa.
21.4.2.
Semntica de copia
C21
Del mismo modo, aunque parece ms evidente, utilizar mtodos en los que se pueden especicar rangos es siempre ms eciente que usar sus equivalentes en los que
slo se proporciona un elemento (muchos mtodos estn sobrecargados para soportar
ambos casos).
[592]
9 void test_copy_semantics(void) {
10
vector<Counter> counters;
11
Counter c1;
12
counters.push_back(c1);
13
Counter c2 = counters[0];
14
15
counters[0].inc();
16
17
assert(c1.get() == 0);
18
assert(counters[0].get() == 1);
19
assert(c2.get() == 0);
20 }
class String {
string value;
string desc;
public:
String(string init) : value(init), desc(init) {
cout << "created: " << desc << endl;
}
String(const String& other) {
value = other.value;
desc = "copy of " + other.desc;
cout << desc << endl;
}
~String() {
cout << "destroyed: " << desc << endl;
}
bool operator<(const String& other) const {
return value < other.value;
}
friend ostream&
operator<<(ostream& out, const String& str) {
out << str.value;
return out;
}
};
void test_copy_semantics(void) {
vector<String> names;
names.push_back(String("foo"));
names.push_back(String("bar"));
names.push_back(String("buzz"));
cout << "-- init ready" << endl;
sort(names.begin(), names.end());
cout << "-- sort complete" << endl;
String i1 = names.front();
cout << "-- end" << endl;
[593]
created: bar
copy of bar
copy of copy of foo
destroyed: copy of foo
destroyed: bar
created: buzz
copy of buzz
copy of copy of copy of
copy of copy of bar
destroyed: copy of copy
destroyed: copy of bar
destroyed: buzz
-- init ready
copy of copy of copy of
destroyed: copy of copy
copy of copy of buzz
destroyed: copy of copy
-- sort complete
copy of copy of copy of
-- end
destroyed: copy of copy
destroyed: copy of copy
destroyed: copy of copy
destroyed: copy of copy
foo
of foo
bar
of copy of bar
of buzz
copy of bar
of
of
of
of
21.4.3.
Extendiendo la STL
La librera STL est especcamente diseada para que se pueda extender y adaptar de forma sencilla y eciente. En esta seccin veremos cmo crear o adaptar nuestros propios contenedores, functors y allocators. Ya vimos como crear un algoritmo en
la seccin 21.3.1.
Creando un contenedor
Dependiendo del modo en que se puede utilizar, los contenedores se clasican por
modelos. A menudo, soportar un modelo implica la existencia de mtodos concretos.
Los siguientes son los modelos ms importantes:
C21
[594]
Forward container
Son aquellos que se organizan con un orden bien denido, que no puede cambiar
en usos sucesivos. La caracterstica ms interesante es que se puede crear ms
de un iterador vlido al mismo tiempo.
Reversible container
Puede ser iterado de principio a n y viceversa.
Random-access container
Es posible acceder a cualquier elemento del contenedor empleando el mismo
tiempo independientemente de su posicin.
Front insertion sequence
Permite aadir elementos al comienzo.
Back insertion sequence
Permite aadir elementos al nal.
Associative container
Aquellos que permiten acceder a los elementos en funcin de valores clave en
lugar de posiciones.
Cada tipo de contenedor determina qu tipo de iteradores pueden aplicarse para
recorrerlo y por tanto qu algoritmos pueden utilizarse con l.
Para ilustrar cules son las operaciones que debe soportar un contenedor se incluye
a continuacin la implementacin de carray. Se trata de una adaptacin (wrapper)
para utilizar un array C de tamao constante, ofreciendo la interfaz habitual de un contenedor. En concreto se trata de una modicacin de la clase carray propuesta inicialmente por Bjarne Stroustrup en su libro The C++ Programming Language [93]
y que aparece en [51].
Listado 21.87: carray: Wrapper STL para un array C
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
T value_type;
T* iterator;
const T* const_iterator;
T& reference;
const T& const_reference;
size_t size_type;
ptrdiff_t difference_type;
// iteradores
iterator begin() { return v; }
const_iterator begin() const { return v; }
iterator end() { return v+thesize; }
const_iterator end() const { return v+thesize; }
// acceso directo
reference operator[](size_t i) { return v[i]; }
const_reference operator[](size_t i) const { return v[i]; }
// size
size_type size() const { return thesize; }
size_type max_size() const { return thesize; }
// conversin a array
[595]
31
T* as_array() { return v; }
32 };
El siguiente listado muestra una prueba de su uso. Como los iteradores de carray
son realmente punteros ordinarios16 , este contenedor soporta los modelos forward y
reverse container adems de random access ya que tambin dispone del operador de
indexacin.
Listado 21.88: carray: Ejemplo de uso de carray
1 void test_carray() {
2
carray<int, 5> array;
3
4
for (unsigned i=0; i<array.size(); ++i)
5
array[i] = i+1;
6
7
reverse(array.begin(), array.end());
8
9
transform(array.begin(), array.end(),
10
array.begin(), negate<int>());
11
12
int expected[] = {-5, -4, -3, -2, -1};
13
14
assert(equal(array.begin(), array.end(), expected));
15 }
Functor adaptables
C21
Los adaptadores que incorpora la librera (ptr_fun(), mem_fun(), etc.) ofrecen suciente exibilidad como para aprovechar los algoritmos genricos utilizando
predicados u operadores implementados como mtodos o funciones. An as, en muchos casos puede ser conveniente escribir functors especcos (ver seccin 21.3.3).
[596]
21.4.4.
Allocators
[597]
Hacer una inicializacin especial de la memoria o alguna operacin de limpieza
adicional.
Para lograrlo la STL proporciona una nueva abstraccin: el allocator. Todos los
contenedores estndar utilizan por defecto un tipo de allocator concreto y permiten
especicar uno alternativo en el momento de su creacin, como un parmetro de la
plantilla.
Usar un allocator alternativo
Como sabemos, todos los contenedores de STL son plantillas que se instancian
con el tipo de dato que van a contener. Sin embargo, tienen un segundo parmetro: el allocator que debe aplicar. Veamos las primeras lneas de al denicin
de vector.
Listado 21.91: Denicin del contenedor vector
1 template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
2 class vector : protected _Vector_base<_Tp, _Alloc>
3 {
4
typedef typename _Alloc::value_type _Alloc_value_type;
Ese parmetro de la plantilla (_Alloc) es opcional porque la denicin proporciona un valor por defecto (std::allocator). El allocator tambin es una
plantilla que se instancia con el tipo de elementos del contenedor.
Si se desea utilizar un allocator basta con indicarlo al instanciar el contenedor:
Listado 21.92: Especicando un allocator alternativo
1 vector<int, custom_alloc> v;
El allocator es una clase que encapsula las operaciones de peticin (a travs del
mtodo allocate()) y liberacin (a travs del mtodo deallocate()) de una
cantidad de elementos de un tipo concreto. La signatura de estos mtodos es:
Listado 21.93: Mtodos bsicos del allocator
1 pointer allocate(size_type n, const void* hint=0);
2 void deallocate(pointer p, size_type n);
C21
Creando un allocator
[598]
3 public:
4
typedef T value_type;
5
typedef T* pointer;
6
typedef const T* const_pointer;
7
typedef T& reference;
8
typedef const T& const_reference;
9
typedef size_t size_type;
10
typedef ptrdiff_t difference_type;
11
12
13
template <typename U>
14
struct rebind {
15
typedef custom_alloc<U> other;
16
};
17
18
custom_alloc() {}
19
20
custom_alloc(const custom_alloc&) {}
21
22
template <typename U>
23
custom_alloc(const custom_alloc<U>&) {}
24
25
pointer address(reference value) const {
26
return &value;
27
}
28
29
const_pointer address(const_reference value) const {
30
return &value;
31
}
32
33
size_type max_size() const {
34
return numeric_limits<size_t>::max() / sizeof(T);
35
}
36
37
pointer allocate(size_type n, const void* hint=0) {
38
return (pointer) (::operator new(n * sizeof(T)));
39
}
40
41
void deallocate(pointer p, size_type num) {
42
delete p;
43
}
44
45
void construct(pointer p, const T& value) {
46
new (p) T(value);
47
}
48
49
void destroy(pointer p) {
50
p->~T();
51
}
Las lneas 4 a 15 denen una serie de tipos anidados que todo allocator debe tener:
value_type
El tipo del dato del objeto que almacena.
reference y const_reference
El tipo de las referencia a los objetos.
pointer y const_pointer
El tipo de los punteros a los objetos.
size_type
El tipo que representa los tamaos (en bytes) de los objetos.
difference_type
El tipo que representa la diferencia entre dos objetos.
[599]
21.4.5.
El nuevo estndar C++11 viene con interesantes novedades a la librera STL. Esta seccin aborda las ms importantes. Veremos cmo el nuevo estndar mejora la
eciencia y simplica algunos aspectos en el uso de la librera.
La funcin bind()
En realidad bind() es una plantilla. Se utiliza para crear un functor a partir de
una funcin, mtodo o functor y un especicacin de argumentos. bind() substituye
a los adaptadores bind1st() y bind2nd(). Permite especicar la posicin en la
que se desea pasar el argumento adicional indicado en el constructor y el de la invocacin al functor resultante. Veamos un ejemplo de cmo bind() puede substituir a
bind2nd() en el listado 21.63.
Listado 21.95: Ejemplo de find_if() usando bind()
C21
1 void test_find_if_cpp11() {
2
using namespace std::placeholders;
3
4
const int size = 5;
5
const int value = 1;
6
int numbers[] = {2, 7, 12, 9, 4};
7
8
assert(find_if(numbers, numbers + size,
9
bind(greater<int>(), _1, 6)) == numbers+1);
[600]
5
6
assert(count_if(enemies.begin(), enemies.end(),
7
bind(&Enemy::is_alive, _1)) == 2);
8 }
Pero bind() hace mucho ms. Permite construir funciones parcialmente especicadas. Una funcin parcialmente especicada es una utilidad tpica de los lenguajes
funcionales. Como su nombre indica, es una forma de jar el valor de uno o ms
parmetros de una funcin (o algo que se comporta como tal). El resultado es una
nueva funcin (un functor realmente) con menos parmetros. Veamos un ejemplo (listado 21.97). Tenemos una funcin que imprime un texto en un color determinado
indicado por parmetro. Utilizando bind() vamos a crear otra funcin que, a partiendo de la primera, permite escribir texto en verde para imprimir un mensaje que
informa de un comportamiento correcto (como en un logger).
Listado 21.97: Una funcin parcialmente especicada
1
2
3
4
5
6
7
8
9
10
bind()
Es el adaptador de funciones denitivo. Substituye a todos los adaptadores que ofreca la STL antes del
estndar C++11.
[601]
30
31
32
33
34
35
36
37
38
39
40
41
42
bind(&Car::forward_left, &car));
listener.add_hook({OIS::KC_S, OIS::KC_D},
bind(&Car::backward_right, &car));
listener.add_hook({OIS::KC_S, OIS::KC_A},
bind(&Car::backward_left, &car));
listener.add_hook({OIS::KC_W},
bind(&Car::forward, &car));
listener.add_hook({OIS::KC_S},
bind(&Car::backward, &car));
listener.add_hook({OIS::KC_ESCAPE},
bind(&WindowEventListener::shutdown, &listener)
);
// ...
Fjese en el atributo triggers_ (lnea 9), el cual consiste en un mapa que relaciona un vector de KeyCode (una combinacin de teclas) con un manejador (tipo
function<void()>). Esto permite cambiar las asignaciones de teclas segn las
preferencias del usuario, leyndolas por ejemplo de un chero de conguracin, y
sera muy sencillo tambin cambiarlas con la aplicacin en marcha si fuese necesario.
Contenedores
Con el nuevo estndar, todos los tipos se pueden inicializar de forma equivalente
a como se haca con los arrays, es decir, escribiendo los datos entre llaves:
Listado 21.99: Inicializacin uniforme: un array C y un vector
1 int numbers[] = {1, 2, 3, 4};
2 std::vector<int> numbers_vector = {1, 2, 3, 4};
Se ha aadido el contenedor forward_list. Se trata de una lista enlazada sencilla, que nicamente soporta iteracin hacia adelante, no permite acceder al nal, ni
siquiera dispone del mtodo size().
C21
[602]
Otra novedad interesante relacionada con la eciencia es la aparicin de los mtodos emplace() y emplace_back() en varios contenedores. emplace_back()
construye un objeto directamente en el contenedor, sin crear un objeto temporal, algo muy conveniente cuando nuestros objetos son complejos. Ejecuta el ejemplo del
listado 21.102 para comprobar el efecto.
Listado 21.102: Uso de emplace() para insercin eciente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct A {
A(int) {
cout << "ctor" << endl;
}
A(const A& other) {
cout << "copy-ctor" << endl;
}
A(A&& other) {
cout << "move-ctor" << endl;
}
};
int main() {
std::vector<A> v;
v.emplace_back(1);
cout << "---" << endl;
v.push_back(A(1));
Iteradores
El nuevo estndar ha incorporado mtodos que retornan iteradores constantes en
todos los contenedores: cbegin(), cend() y tambin iteradores inversos contantes: crbegin() y crend(). Adems estn disponibles versiones de begin() y
end() como funciones. De ese modo es posible escribir cdigo genrico que funciona con una referencia a cualquier contenedor, incluyendo arrays C.
Algoritmos
Se han incorporado varios algoritmos nuevos. Aqu se incluye un resumen de algunos de ellos:
all_of() Devuelve cierto slo si el predicado es cierto para todos los elementos
de la secuencia.
any_of() Devuelve cierto si el predicado es cierto para al menos uno de los elementos de la secuencia.
none_of() Devuelve cierto slo si el predicado es falso para todos los elementos
de la secuencia.
iota() Asigna valores crecientes a los elementos de una secuencia.
minmax() Devuelve los valores mnima y mximo de una secuencia.
is_sorted() Devuelve cierto si la secuencia est ordenada.
21.5.
[603]
21.5.1.
GCC y Clang son los dos compiladores que dan soporte a mayor nmero de
caractersticas del nuevo estndar. En el momento de escribir esta documentacin, la
ltima versin estable de GCC es la 4.8.2, con la que se puede disfrutar de todas las
novedades del ncleo del lenguaje y muchas de la biblioteca estndar.
Para compilar un programa de C++ usando el nuevo estndar hay que utilizar la
opcin -std=c++11 al compilar. Por ejemplo:
g++ -o main main.cc -std=c++11
Normalmente, si no se utiliza esta opcin, GCC compilar usando el estndar
C++03. Clang se usa exactamente de la misma forma.
Si se usa la librera estndar hay que linkarla (con g++ se puede omitir pues lo
hace de forma automtica):
21.5.2.
Expresiones constantes
Un compilador de C++ es capaz de optimizar ciertas expresiones que sern siempre constantes, por ejemplo:
1
2
3
4
int a = 1 + 2;
cout << 3.2 - 4.5 << endl;
int miArray[4 * 2];
C21
[604]
Inicializador de listas
Antes de la entrada del nuevo estndar, la inicializacin de los contenedores de la
STL era posible utilizando una zona de memoria con una secuencia de elementos del
se podra inicializar un vector como sigue (tambin se incluyen ejemplos con enteros).
1
2
3
4
5
6
7
8
[605]
Esto es posible gracias al uso de usa nueva sintaxis y del contenedor std::initializer_list.
Si se utiliza como parmetro en el constructor de una clase
Listado 21.103: Clase que utiliza un inicializador de listas
1
2
3
4
5
6
7
class LODDistancias {
public:
LODDistancias(std::initializer_list<int> entrada) :
distancias(entrada) {}
private:
vector<int> distancias;
};
Hay que tener en cuenta que este tipo de contenedores se utilizan en tiempo de
compilacin, que slo pueden ser construidos estticamente por el compilador y que
no podrn ser modicados en tiempo de ejecucin. Aun as, como son un tipo, pueden
ser utilizados en cualquier tipo de funciones.
1
2
3
4
5
Inicializacin uniforme
En C++03 no existe una forma uniforme de inicializar los objetos. En el aparatado
anterior, en la parte compatible con el antiguo estndar, se ha utilizado la inicializacin
de un array utilizando . Esto es posible ya que esa estructura es un agregado18 , ya que
slo este tipo de objetos y los arrays pueden ser inicializados de esta manera.
Con la aparicin de C++11, es posible utilizar las llaves para inicializar cualquier
clase o estructura. Por ejemplo, supongamos una clase para representar un vector de
tres dimensiones.
18 Un agregado (aggregate ) es una clase o estructura que no tiene destructor denido por el usuario ni
operador de asignacin. Tampoco tendrn miembros privados o protegidos que no sean estticos, ni una
clase base o funciones virtuales.
C21
Tambin se pueden utilizar las llaves junto con el operador =, para inicializar o
para asignar nuevos valores.
[606]
1
2
3
4
5
6
7
8
9
10
11
12
class Vector3D {
public:
Vector3D(float x, float y, float z):
_x(x), _y(y), _z(z) {}
private:
float _x;
float _y;
float _z;
friend Vector3D normalize(const Vector3D& v);
};
La primera utiliza la nueva inicializacin uniforme, la segunda la clsica, invocando el constructor de forma explcita.
En C++11 tambin es posible utilizar esta inicializacin para construir de manera
implcita objetos que son devueltos por una funcin. El compilador utilizar el valor
de retorno del prototipo de la funcin y lo usar junto con los valores proporcionados
para construir y devolver un objeto de dicho tipo.
1 Vector3D normalize(const Vector3D& v){
2
float len = sqrt(v._x*v._x + v._y*v._y + v._z*v._z);
3
4
return {v._x/len, v._y/len, v._z/len};
5 }
Esta notacin no sustituye a la anterior. Cabe destacar que cuando se utiliza esta
sintaxis para inicializar un objeto, el constructor que acepta una lista de inicializacin
como las presentadas anteriormente tendr prioridad sobre otros. Debido a esto, algunas veces ser necesario invocar directamente al constructor adecuado con la notacin
antigua.
Esta forma de devolver objetos es compatible con RVO (Return Value Optimization), que se ver en optimizaciones. Con lo cual una llamada como la siguiente
generar cdigo ptimo y seguramente sin ninguna copia.
1
Vector3D p2 = normalize(p);
Inferencia de tipos
Hasta ahora cada vez que se declaraba una variable en C++ haba que especicar
de qu tipo era de manera explcita. En C++11 existe la inferencia de tipos. Usando la
palabra reservada auto en una inicializacin en vez del tipo, el compilador deducir
el mismo de manera automtica.
En el ejemplo siguiente se ve cmo funciona esta caracterstica, tanto para tipos
bsicos como para la clase denida en el apartado anterior.
1
2
3
[607]
Esta nueva caracterstica es especialmente adecuada para simplicar algunas declaraciones complejas. A continuacin se muestra la diferencia en la declaracin del
iterador al recorrer un contenedor.
1
2
3
4
5
6
Existe otra palabra reservada que se usa de forma similar a sizeof(), pero que
devuelve el tipo de una variable. Esta palabra es decltype y se puede usar para
extraer el tipo de una variable y usarlo para la declaracin de otra.
1
Funciones Lambda
Las funciones lambda son simplemente funciones annimas. La sintaxis para declarar este tipo de funciones es especial y es posible no declarar el tipo devuelto de
manera explcita sino que est denido de forma implcita mediante la construccin
decltype(<expresin_devuelta>). Las dos formas posible de declarar y
denir estas funciones son las siguientes.
[captura](parmetros)->tipo_de_retorno{cuerpo}
[captura](parmetros){cuerpo}
La primera hace explcito el tipo que se devuelve. De esto modo, las funciones que
se muestran a continuacin son equivalentes.
1
2
C21
En el ejemplo anterior se utiliza una referencia para evitar la copia y la penalizacin de rendimiento.
[608]
Las variables que se utilizan dentro de estas funciones pueden ser capturadas para
utilizarse en el exterior. Se pueden capturar por valor o por referencia, dependiendo de
la sintaxis dentro de []. Si se utiliza por ejemplo [p1, &p2], p1 ser capturada por
valor y p2 por referencia. Si se usa [=,&p1], todas las variables sern capturadas por
valor (al usar =) excepto p1 que ser capturada por referencia. Si se utiliza [&,p2],
todas se capturarn por referencia (usando &), excepto p2.
En el siguiente ejemplo, se utiliza una funcin lambda para sumar la puntaciones de todos los jugadores, que han sido previamente almacenadas en una lista. Se
muestran tres formas de hacerlo.
1
2
3
4
5
6
7
8
9
10
11
12
class K {
public:
int operator*(const K& k) const {return 2;}
};
template <typename T>
T pow2Bad(const T& t){return t*t;}
template <typename T>
auto pow2(const T& t)->decltype(t*t){return t*t;}
K kObj;
cout << pow2Bad(kObj) << endl; // <- no compila
cout << pow2(kObj)
<< endl;
[609]
class playerInfo {
public:
playerInfo(const string& name) :
_name(name) {}
playerInfo() : playerInfo("default") {}
private:
string _name;
};
En el ejemplo anterior tambin se muestra (lneas 4-6 y 22 ) el uso de final.
Cuando se utiliza en la declaracin de un mtodo, indica que ninguna clase que herede
de sta podr sobrescribirlo.
C21
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
[610]
Puntero null
Se introduce tambin un nuevo valor slo asignable a punteros: nullptr. Este
valor no se puede asignar a ningn otro tipo. En C++03, se usaba el valor 0 para los
punteros null, de este modo, se poda asignar el valor de estos punteros a un entero o
a un booleano. Con nullptr esto ya no es posible, ayudando a prevenir errores y a
sobrescribir funciones.
1
int* c = nullptr;
Cabe destacar que es un tipo compatible con los booleanos, pero que no es compatible con los enteros.
1
2
Tambin se puede utilizar la nueva sintaxis para realizar las deniciones de tipo
que se hacan con typedef.
[611]
class Vector3D {
public:
Vector3D(float x, float y, float z) {}
};
union miUnion {
int
a;
float
b;
Vector3D v;
};
Tambin se permite la construccin de cadenas raw, que no interpretarn los caracteres de escape (_), ni las propias comillas ("). Para denir este tipo de cadenas
se usa R"(literal)". Tambin es posible usar cadenas raw con la modicacin
Unicode.
1
2
3
4
C21
1
2
3
[612]
A partir de ahora se pueden crear nuevos literales usando sujos. Los sujos podrn ir detrs de nmeros (los que puedan ser representados por unsigned long
long o long double) o detrs de literales de cadena. Estos sujos corresponden a
funciones con el prototipo retval operator _sufijo ( unsigned long
long ).
1 double operator"" _d (unsigned long long i) {
2
return (double) i;
3 }
La funcin anterior dene el sujo _d, que podr ser usado para crear un double
usando un nmero natural como literal.
1
auto d = 30_d;
Un ejemplo un poco ms complejo del uso de este operador se expone a continuacin. Sea la siguiente una clase que podra representar un vector de tres dimensiones,
incluyendo la operacin de suma.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vector3D {
public:
Vector3D() :
x_(0), y_(0), z_(0) {} ;
Vector3D(float x, float y, float z) :
x_(x), y_(y), z_(z) {} ;
Vector3D operator+(const Vector3D& v) {
return Vector3D(x_ + v.x_,
y_ + v.y_,
z_ + v.z_ );
}
private:
float x_;
float y_;
float z_;
friend Vector3D operator"" _vx(long double x);
friend Vector3D operator"" _vy(long double y);
friend Vector3D operator"" _vz(long double z);
};
Se podran denir los siguientes literales de usuario, por ejemplo para construir
vectores ortogonales.
1
2
3
4
5
6
7
8
9
10
11
[613]
Para utilizar los sujos con los literales de cadenas, se muestra el siguiente ejemplo, que representa un jugador, con un nombre.
1
2
3
4
5
6
7
8
9
10
11
12
class Player {
public:
Player(string name):
name_(name) {}
private:
string name_;
};
Player operator"" _player(const char* name, size_t nChars) {
return Player(name);
};
Aserciones estticas
Algo muy til que ya inclua Boost es una asercin esttica. Este tipo de aserciones
se comprobarn en tiempo de compilacin, y ser el mismo compilador el que avise
de la situacin no deseada.
En C++11 se puede usar static_assert (cont-expr, error-message)
para utilizar estas aserciones en cualquier punto del cdigo.
template <typename T>
bool equal(T a, T b, T epsilon) {
static_assert( sizeof(T) >= 8, "4 bytes como poco" );
}
return 0;
C21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[614]
Eliminacin y seleccin por defecto explcita de funciones
En C++11 es posible prohibir el uso de las funciones de una clase, incluyendo
los constructores, haciendo uso de la palabra reservada delete. Esto es muy til
para evitar que alguien use un constructor no deseado (en C++03 se declaraba como
privado para obtener el mismo resultado).
1 class NoCopiable {
2 public:
3
NoCopiable(){}
4
NoCopiable(const NoCopiable&) = delete;
5
NoCopiable& operator=(const NoCopiable&) = delete;
6 };
En el mismo ejemplo se usa default con uno de los constructores, lo que pide
de forma explcita al compilador que l cree uno por defecto.
Constructores de movimiento
Se introduce el concepto de constructor de movimiento, en contraste con el aun
necesario constructor de copia. Mientras que es este ltimo se usa para determinar la
forma en la que se copian los objetos, el de movimiento determina qu signica mover
las propiedades de un objeto a otro (no son dos objetos independientes).
Aunque sea de forma transparente al programador, el compilador genera variables
temporales para realizar determinadas operaciones. El constructor de movimiento es
una forma de evitar este tipo de variables intermedias (de copia) y as poder optimizar
determinadas operaciones (asignaciones normalmente).
Se introduce tambin el concepto de referencias-rvalue (&&). Ya que en esta seccin se introducen muchas caractersticas, esta en concreto slo se va a mencionar
por encima, puesto que profundizar en ella podra llevar tanto como para el resto juntas. Como resumen, decir sobre ellas que son referencias especiales que apuntan a
variables sin una direccin de memoria (variables temporales).
El constructor de movimiento se declara como el de copia, pero el parmetro de
entrada usa &&. Lo mismo se aplica para la versin anloga del operador de asignacin.
[615]
return *this;
~Movible()
{
cout << "Destructor" << endl;
if (buffer_ == nullptr)
cout << "--> con nullptr (moviendo)" << endl;
}
delete [] buffer_;
C21
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
52
53
54
55
56
57
[616]
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
private:
char*
buffer_;
unsigned size_;
};
Movible getM()
{
cout << "getM()" << endl;
Movible nuevo_objecto(20000);
return nuevo_objecto;
}
int main(int argc, char *argv[])
{
vector<Movible> v;
Movible k(234303);
k = getM();
v.push_back(Movible(4000));
}
return 0;
Cuando se usa la asignacin, el objeto temporal que se genera para devolver por
copia un objecto, es capturado por la asignacin con movimiento y no se realiza ninguna copia extra.
La biblioteca estndar est preparada para el uso de constructores de movimiento,
y como se ve en el ejemplo anterior, lo que en c++03 supondra una copia por cada
push_back() en c++11 supone una llamada transparente al constructor de movimiento, evitando as todas las copias que se realizaran de otra forma.
Ntese como en los movimientos se ejecuta el destructor del objeto,
21.5.3.
[617]
Los posible motores a utilizar son: std::linear_congruential (generador linear congruencial), std::subtract_with_carry (resta con acarreo) y
std::mersenne_twister, que se representan con plantillas. Existen deniciones de tipo, para poder usarlas sin congurar cada parmetro de las mismas: minstd_rand0 y minstd_rand (lineales), mt19937 y mt19937_64 (mersenne
twister), y ranlux24_base, ranlux48_base y ranlux24 (resta con acarreo).
Las distribuciones: uniform_int_distribution,
bernoulli_distribution, geometric_distribution,
poisson_distribution, binomial_distribution,
uniform_real_distribution, exponential_distribution,
normal_distribution y gamma_distribution.
En el siguiente ejemplo se muestra un posible uso, sacando la semilla del reloj del
sistema en este caso.
#include
#include
#include
#include
<iostream>
<functional>
<random>
<sys/time.h>
// Uso directo
return 0;
Tablas Hash
Se introducen 4 tipos de tablas hash: std::unordered_set, std::unordered_multiset, std::unordered_map y std::unordered_multimap.
La que se corresponde con el concepto tradicional en la que est representada por
std::unordered_map. Ya que su uso es similar a std::map y al resto de contenedores asociativos antiguos, simplemente se muestra un ejemplo
a continuacin.
#include <iostream>
#include <unordered_map>
C21
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
[618]
10 }
Expresiones regulares
Una de las caractersticas nuevas ms interesantes que se han aadido al estndar son las expresiones regulares. Para ello se utiliza un objeto std::regex para
construir la expresin.
1
2
std::smatch match;
Es posible buscar todas las coincidencias dentro de una cadena como se muestra a
continuacin.
1
2
3
4
5
6
7
8
if (regex_match(entrada, eRE2))
cout << "[" << entrada << "] cumple la regex" << endl;
#include <iostream>
#include <tuple>
using namespace std;
typedef tuple <string, int, float> tuplaPuntos;
int main(int argc, char *argv[])
{
tuplaPuntos p1("Bilbo", 20, 35.0);
cout << "El jugador "
<< get<0>(p1)
<< " ha conseguido " << get<2>(p1)
21.6. Plugins
[619]
14
<< " puntos en "
15
<< " jugadas"
16
17
return 0;
18 }
<< get<1>(p1)
<< endl;
Otras caractersticas
Aparte de las caractersticas mencionadas en las secciones anteriores, C++11 incluye una biblioteca para el uso de traits para la metaprogramacin, tambin envoltorios para poder utilizar referencias en plantillas, mtodos uniformes para calcular el
tipo devuelto en objetos funciones y soporte multitarea.
21.6.
Plugins
C21
Permite a empresas o colectivos concretos implementar funcionalidades a la medida de sus necesidades, que normalmente no seran admitidas en la aplicacin
principal.
[620]
21.6.1.
Para ejecutarlo hay que indicarle al sistema operativo que tambin tiene que buscar bibliotecas en el directorio actual. Para eso basta denir la variable de entorno
LD_LIBRARY_PATH.
$ LD_LIBRARY_PATH=. ./main
mylib_func test 12345
Sin tocar para nada todo lo hecho hasta ahora, vamos a generar otra biblioteca
dinmica con la misma funcin denida de otra forma.
21.6. Plugins
[621]
Listado 21.108: Otra implementacin de la biblioteca mnima (mylib2.c)
1 #include <stdio.h>
2
3 void mylib_func(const char* str, int val) {
4
printf("cambiada mylib_func %d %s\n", val, str);
5 }
Sorprendido? No hemos recompilado el programa, no hemos cambiado la biblioteca original, pero hemos alterado el funcionamiento. Esta tcnica puede utilizarse
para multitud de nes, desde la depuracin (e.g. ElectricFence) hasta la alteracin de
los ejecutables para corregir errores cuando no se dispone del cdigo fuente.
Lo que pasa tras el teln podemos analizarlo con herramientas estndar:
$ ldd main
linux-vdso.so.1 => (0x00007fff701ff000)
libmylib.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f13043dd000)
/lib64/ld-linux-x86-64.so.2 (0x00007f130477c000)
#include <stdio.h>
void mylib_func(const char* str, int val);
int main_i = 54321;
void main_func(int v) {
printf("main_func %d\n", v);
}
int main() {
mylib_func("test", 12345);
return 0;
}
C21
Todos los ejecutables dinmicos estn montados con la biblioteca ld.so o ld-linux.so. Se trata del montador dinmico. Obsrvese cmo se incluye en el ejecutable la ruta completa (ltima lnea). Esta biblioteca se encarga de precargar las
bibliotecas especicadas en LD_PRELOAD, buscar el resto en las rutas del sistema o
de LD_LIBRARY_PATH, y de cargarlas. El proceso de carga en el ejecutable incluye
resolver todos los smbolos que no estuvieran ya denidos.
[622]
#include <stdio.h>
void main_func(int v);
extern int main_i;
void mylib_func(const char* str, int val) {
printf("mylib_func %d %s\n", val, str);
main_func(main_i);
}
Si todas estas actividades son realizadas por una biblioteca (ld.so) no debera
extraar que esta funcionalidad est tambin disponible mediante una API, para la
carga explcita de bibliotecas desde nuestro programa.
21.6.2.
21.6. Plugins
[623]
Vamos a seguir un ejemplo muy sencillo en las prximas secciones. El ejemplo
est formado por un programa principal que tiene la lgica de registro de los plugins
(main.c), una biblioteca esttica (liba) y una (libb) que se cargar dinmicamente. Ambas bibliotecas tienen un chero de cabecera (a.h y b.h) y dos cheros
de implementacin cada una (a.c, a2.c, b.c y b2.c). La funcionalidad es absolutamente trivial y sirve nicamente para ilustrar la ejecucin de las funciones correspondientes.
Listado 21.111: Biblioteca esttica liba: a.h
1
2
3
4
5
6
7
#ifndef A_H
#define A_H
1
2
3
4
5
6
#include <stdio.h>
#include "a.h"
void a(int i) {
printf("a( %d) returns %d\n", i, a2(i));
}
1
2
3
4
5
6
7
#ifndef B_H
#define B_H
1
2
3
4
5
6
#include <stdio.h>
#include "b.h"
void b(int i) {
printf("b( %d) returns %d\n", i, b2(i));
}
C21
[624]
2
3 int b2(int i) {
4
return i * i;
5 }
CC = gcc
CFLAGS = -Wall -ggdb -fPIC
LDFLAGS = -fPIC -shared
TARGET = libb.so.1.0.0
all: $(TARGET)
$(TARGET): b.o b2.o
$(CC) -Wl,-soname,libb.so.1.0.0 $(LDFLAGS) -o $@ $^
clean:
$(RM) *.o *~ *.a $(TARGET)
Carga explcita
En primer lugar veamos cmo cargar y ejecutar un smbolo (la funcin b()) de
forma explcita, es decir, el programador utiliza libdl para buscar la biblioteca y
cargar el smbolo concreto que desea:
Listado 21.118: Carga explcita de smbolos con libdl: main.c
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
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "a.h"
#define LIBB_PATH "./dirb/libb.so.1.0.0"
void error() {
fprintf(stderr, dlerror());
exit(1);
}
int main() {
int i = 3;
void *plugin;
void (*function_b)(int);
if ((plugin = dlopen(LIBB_PATH, RTLD_LAZY)) == NULL)
error();
if ((function_b = dlsym(plugin, "b")) == NULL)
error();
printf("Results for %d:\n", i);
a(i);
function_b(i);
dlclose(plugin);
return 0;
21.6. Plugins
[625]
La diferencia ms importante respecto al uso habitual de una biblioteca dinmica
es que no hay ninguna referencia a libb en la construccin del programa main.c
del listado anterior. Veamos el Makefile de la aplicacin:
Listado 21.119: Carga explcita de smbolos con libdl: Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CC = gcc
CFLAGS = -Wall -ggdb -Idira
LDFLAGS = -Ldira
LDLIBS = -la -ldl
all: libs main
main: main.o
libs:
$(MAKE) -C dira
$(MAKE) -C dirb
clean:
$(RM) main *.o *~
$(MAKE) -C dira clean
$(MAKE) -C dirb clean
Carga implcita
Veamos ahora cmo construir un sencillo mecanismo que cargue automticamente
el smbolo en el momento de solicitar su uso.
Listado 21.120: Carga implcita de smbolos con libdl: plugin.c
typedef struct plugin {
char* key;
void (*function)(int);
struct plugin* next;
} plugin_t;
static plugin_t* plugins;
void
plugin_register(char* key, void (*function)(int)) {
plugin_t* p = (plugin_t*) malloc(sizeof(plugin_t));
p->key = key;
p->function = function;
p->next = plugins;
plugins = p;
printf("** Plugin %s successfully registered.\n", key);
}
void
plugin_unregister(char* key) {
plugin_t *prev = NULL, *p = plugins;
while (p) {
if (0 == strcmp(p->key, key))
break;
prev = p;
p = p->next;
if (!p)
return;
if (prev)
C21
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
[626]
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
free(p);
static plugin_t*
plugin_find(char* key) {
plugin_t* p = plugins;
while (p) {
if (0 == strcmp(p->key, key))
break;
p = p->next;
}
return p;
void
call(char* key, int i) {
plugin_t* p;
p = plugin_find(key);
if (!p) {
char libname[PATH_MAX];
sprintf(libname, "./dir %s/lib %s.so", key, key);
printf("Trying load %s.\n", libname);
dlopen(libname, RTLD_LAZY);
p = plugin_find(key);
}
if (p)
p->function(i);
else
fprintf(stderr, "Error: Plugin %s not available.\n", key);
Los plugins (lneas 15) se almacenan en una lista enlazada (lnea 7). Las funciones plugin_register() y plugin_unregister() se utilizan para aadir y
eliminar plugins a la lista. La funcin call() (lneas 5471) ejecuta la funcin especica (contenida en el plugin) que se le pasa el parmetro, es decir, invoca una funcin
a partir de su nombre19 . Esa invocacin se puede ver en la lnea 10 del siguiente listado:
Listado 21.121: Carga implcita de smbolos con libdl: main.c
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include "plugin.h"
#include "a.h"
int main() {
int i = 3;
Para que los plugins (en este caso libb) se registren automticamente al ser cargados se requiere un pequeo truco: el atributo constructor (lnea 9) que provoca
que la funcin que lo tiene se ejecute en el momento de cargar el objeto:
19 Este
21.6. Plugins
[627]
Listado 21.122: Carga implcita de smbolos con libdl: b.c
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include "../plugin.h"
#include "b.h"
void b(int i) {
printf("b( %d) returns %d\n", i, b2(i));
}
static void init() __attribute__((constructor));
static void init() {
plugin_register("b", &b);
}
El atributo constructor indica que el smbolo al que va asociado debe almacenarse en una seccin de la biblioteca reservada para el cdigo de los constructores de variables estticas. Estos constructores deben ejecutarse tan pronto
como la biblioteca se carga en memoria. Anlogamente, la seccin destructor
aglutina los destructores de las variables estticas, que se invocan tan pronto
como la biblioteca es cerrada.
21.6.3.
GModule ofrece un API muy similar a libdl con funciones prcticamente equivalentes:
1
2
3
4
5
C21
[628]
Carga explcita
El siguiente listado muestra cmo hacer la carga y uso de la funcin b(), equivalente al listado 21.118:
Listado 21.123: Carga explcita de smbolos con GModule: main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
#include
#include
#include
<stdio.h>
<glib.h>
<gmodule.h>
"a.h"
18
19
20
21
22
23
24
25
26
27
28
29
30 }
Carga implcita
Por ltimo, este mdulo implementa el sistema de registro y carga automtica
usando una tabla hash de glib para almacenar los plugins:
Listado 21.124: Carga explcita de smbolos con GModule: plugin.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include
#include
#include
#include
<stdio.h>
<gmodule.h>
<glib/ghash.h>
"a.h"
#ifndef PATH_MAX
#define PATH_MAX 1024
#endif
static GHashTable* plugins = NULL;
void
plugin_register(char* key, void (*f)(int)) {
if (plugins == NULL)
plugins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free
, g_free);
16
g_hash_table_insert(plugins, key, f);
17
g_message("Plugin %s succesfully registered.", key);
21.6. Plugins
[629]
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
}
void
plugin_unregister(const char* key) {
if (plugins != NULL)
g_hash_table_remove(plugins, key);
}
void
call(const char* key, int i) {
void (*p)(int) = NULL;
if (plugins != NULL)
p = g_hash_table_lookup(plugins, key);
if (!p) {
char libname[PATH_MAX];
sprintf(libname, "./dir %s/lib %s.so", key, key);
g_message("Trying load %s.", libname);
if (g_module_open(libname, G_MODULE_BIND_LAZY) == NULL)
g_error("Plugin %s not available", libname);
if (plugins != NULL)
p = g_hash_table_lookup(plugins, key);
if (!p)
g_error("Plugin %s not available", key);
}
p(i);
21.6.4.
LIBB_PATH = "./dirb/libb.so.1.0.0"
import ctypes
plugin = ctypes.cdll.LoadLibrary(LIBB_PATH)
plugin.b(3)
21.6.5.
C21
[630]
Recordemos el patrn factory method ya descrito en el mdulo 1. Se basa en la denicin de una interfaz para crear instancias de objetos, permitiendo que las subclases
redenan este mtodo. Este patrn se utiliza frecuentemente acoplado con la propia
jerarqua de objetos, de forma parecida al patrn prototype, dando lugar a lo que se
conoce como constructor virtual. Veamos un ejemplo similar al que ponamos para
ilustrar el patrn prototipo, pero ahora empleando el patrn factory method.
Listado 21.126: Ejemplo de patrn factory method
1 class weapon {
2 public:
3
typedef shared_ptr<weapon> weapon_ptr;
4
virtual weapon_ptr make() = 0;
5
virtual void shoot() = 0;
6
virtual ~weapon() {}
7 };
class weapon_factory {
public:
typedef shared_ptr<weapon> weapon_ptr;
weapon_ptr make(const string& key) {
weapon* aux = factories_[key];
if (aux)
return aux->make();
}
return 0;
Para aadir o eliminar nuevas subclases de weapon tenemos que llamar a reg()
o unreg() respectivamente. Esto es adecuado para la tcnica RAII en la que la creacin y destruccin de un objeto se utiliza para el uso y liberacin de un recurso:
Listado 21.128: Ejemplo de RAII para registro de nuevas armas
1 template <class weapon_type>
2 class weapon_reg {
3
weapon_factory& factory_;
4
const char* key_;
5 public:
6
weapon_reg(weapon_factory& factory, const char* key)
21.6. Plugins
[631]
7
8
9
10
11
12
13 };
: factory_(factory), key_(key) {
factory_.reg(key_, new weapon_type());
}
~weapon_reg() {
factory_.unreg(key_);
}
Tanto la factora como los objetos de registro podran ser tambin modelados con
el patrn singleton, pero para simplicar el ejemplo nos limitaremos a instanciarlos
sin ms:
Listado 21.129: Instancias de la factora extensible y una factora concreta
1 using namespace std::placeholders;
El cdigo de un plugin es completamente anlogo al de las otras factoras concretas, como rifle.
Listado 21.131: Ejemplo de plugin para la factora extensible
1
2
3
4
5
6
7
8
9
10
#include "fmethod.hh"
class bow: public weapon {
public:
weapon_ptr make() { return weapon_ptr(new bow); }
void shoot() { cout << "shoot arrow." << endl; }
};
extern dynamic_weapon_factory dfactory;
weapon_reg<bow> bow_reg(dfactory, "bow");
C21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[632]
La variable dfactory es la instancia de la factora dinmica extensible. Est declarada en el programa principal, as que para poder ser utilizada desde una biblioteca
es preciso que el linker monte el programa principal con la opcin -rdynamic. Por
ltimo pondremos un ejemplo de uso de la factora:
Listado 21.132: Ejemplo de uso de la factora
1 void
2 load_and_shoot(const string name) {
3
shared_ptr<weapon> w = dfactory.make(name);
4
if (w)
5
w->shoot();
6
else
7
cout << "Missing weapon " << name << endl;
8 }
21.6.6.
Plugins multi-plataforma
#include "fmethod-win.hh"
extern dynamic_weapon_factory dfactory;
int main(int argc, char* argv[]) {
while (argc > 1) {
shared_ptr<weapon> w = dfactory.make(argv[1]);
if (w)
w->shoot();
else
cout << "Missing weapon " << argv[1] << endl;
argc--; ++argv;
21.6. Plugins
[633]
Listado 21.134: Biblioteca comn con la factora para Windows
1
2
3
4
5
6
7
8
9
10
11
12
#include "fmethod-win.hh"
#include <windows.h>
void
dynamic_weapon_factory::load_plugin(const string& key)
{
string libname = "./fmethod-" + key + "-win.dll";
LoadLibrary(libname.c_str());
}
dynamic_weapon_factory dfactory;
weapon_reg<rifle> rifle_reg(dfactory, "rifle");
#include "fmethod-win.hh"
class bow: public weapon {
public:
weapon_ptr make() { return weapon_ptr(new bow); }
void shoot() { cout << "shoot arrow." << endl; }
};
extern dynamic_weapon_factory dfactory;
weapon_reg<bow> bow_reg(dfactory, "bow");
La opcin del montador -enable-runtime-pseudo-reloc permite utilizar la semntica tradicional de visibilidad de smbolos de Unix. Todos los smbolos externos son automticamente exportados. La opcin -enable-auto-import
permite que todos los smbolos usados en el ejecutable que no estn denidos en el
propio ejecutable sean automticamente importados.
C21
Para compilar y probar todo no es necesario utilizar ReactOS o Microsoft Windows. Podemos usar el compilador cruzado GCC para MINGW32 y el emulador
wine.
Captulo
22
Tcnicas especcas
Francisco Moya Fernndez
Flix J. Villanueva Molina
Sergio Prez Camacho
David Villa Alises
22.1.
Serializacin de objetos
La serializacin de objetos tiene que ver en parte con la persistencia del estado
de un videojuego. Serializar un objeto consiste en convertirlo en algo almacenable y
recuperable. De esto modo, el estado completo del objeto podr ser escrito en disco
o ser enviado a travs de la red, y su estado podr ser recuperado en otro instante de
tiempo o en otra mquina.
Puede parecer una operacin sencilla, despus de todo, bastara con almacenar el
pedazo de memoria que representa al objeto y volver a ponerlo en el mismo sitio despus. Lamentablemente esto no es posible, puesto que la conguracin de la memoria
vara de ejecucin en ejecucin y de mquina en mquina. Adems, cada objeto tiene
sus particularidades. Por ejemplo, si lo que se desea serializar es una std::string
seguramente sea suciente con almacenar los caracteres que la componen.
Uno de los problemas a la hora de serializar objetos es que estos pueden contener
referencias o punteros a otros objetos, y este estado ha de conservarse de forma dedigna. El problema de los punteros, es que la direcciones de memoria que almacenan
sern diferentes en cada ejecucin.
Antes de hablar de la serializacin propiamente dicha, se presentarn los streams
de C++.
635
[636]
22.1.1. Streams
Un stream es como una tubera por donde uyen datos. Existen streams de entrada o de salida, y de ambas, de modo que un programa puede leer (entrada) de una
abstrayndose por completo de qu es lo que est llenando la misma. Esto hace que
los streams sean una forma de desacoplar las entradas de la forma de acceder a las
mismas, al igual que las salidas. No importa si quien rellena un stream es la entrada
del teclado o un archivo, la forma de utilizarla es la misma para ambos casos. De este
modo, controlar la entrada supondra conectar un stream a un chero (o al teclado)
y su salida al programa. Justo al revs (donde el teclado sera ahora la pantalla) se
controlara la salida.
Normalmente los streams tienen un buffer asociado puesto que escribir o leer en
bloques suele ser mucho ms eciente en los dispositivos de entrada y salida. El stream
se encargar (usando un streambuf1 ) de proporcionar o recoger el nmero de bytes
que se requiera leer o escribir en el mismo
En la gura 22.1 se muestra la jerarqua de streams en la biblioteca estndar de
C++. La clase ios_base representa la propiedades generales de un stream, como por
ejemplo si este es de entrada o de salida o si es de texto o binaria. La clase ios, que
hereda de la anterior, contiene un streambuf. Las clases ostream y istream,
derivan de ios y proporcionan mtodos de salida y de entrada respectivamente.
istream
La clase istream implementa mtodos que se utilizan para leer del buffer interno de manera transparente. Existen dos formas de recoger la entrada: formateada
y sin formatear. La primera usa el operador >> y la segunda utiliza los siguientes
miembros de la clase:
1 streambuf es una clase que provee la memoria para dicho buffer incluyendo adems funciones para
el manejo del mismo (rellenado, ushing, etc. . . )
[637]
gcount
get
getline
ignore
peek
read
readsome
putback
unget
ifstream y ofstream
Estos streams que se utilizan para leer y escribir de archivos.
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main(int argc, char *argv[])
{
ifstream infile("prueba.txt", ios_base::binary);
if (!infile.is_open()) {
cout << "Error abriendo fichero" << endl;
return -1;
}
string linea;
getline(infile, linea);
cout << linea << endl;
char buffer[300];
C22
[638]
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 }
En la lnea 9 de crea
del chero, y se intenta abrir para lectura como
el stream
un chero binario. En 11-14
secomprueba que el archivo se abri y se termina el
programa si no es as. En 16-18 se usa una funcin global de string para rellenar
una de estas con una lnea desde
elchero. Se hace lo mismo con un buffer limitado
20-22
. Despus
se leen 3 caracteres sueltos (sin tener en
cuenta el nal de linea) (24-26 ). En 28-31 se juega con la posicin del puntero de
lectura, y en el resto, se lee carcter a carcter hasta el nal del archivo.
Los modos de apertura son los siguientes:
in
out
ate
app
trunc
binary
extraccin.
6-7
)
ya
que
necesitan
acceso a los atributos privados.
La forma de utilizarlos es la siguiente:
Listado 22.4: Operadores de insercin y extraccin
1 int main(int argc, char *argv[])
2 {
3
Vector3D v(1.0, 2.3, 4.5);
4
cout << v << endl;
5
cin >> v ;
6
cout << v << endl;
7
return 0;
8 }
[639]
#include <fstream>
using namespace std;
int main()
{
fstream in ("prueba.txt", ios_base::in | ios_base::binary);
fstream out("copiaP.txt", ios_base::out | ios_base::binary
| ios_base::trunc );
if (!in.is_open() || !out.is_open())
return -1;
in.seekg(0, ios_base::end);
size_t size = in.tellg();
in.seekg(0, ios_base::beg);
char* buffer = new char[size];
in.read (buffer, size);
out.write(buffer, size);
delete [] buffer;
return 0;
}
de un vector con el mismo formato. Al pulsar RETURN el vector original se rellenar con los nuevos datos tomados de la entrada estndar. De hecho, el programa
funciona tambin con una tubera del tipo echo "(1.0, 2.912, 3.123)"|
./ejecutable.
stringstream
En las lneas 6-12 se dene una funcin templatizada que se usa en 33 para
transformar un nmero en una cadena, usando streamstream e invocando luego
su mtodo str(), que devuelve la cadena asociada.
para extraer un nmero de una cadeEn 14-23 se dene otra que se puede
utilizar
na. Se ve un ejemplo de uso en la lnea 34 .
22.1.2.
C22
[640]
#include <iostream>
using namespace std;
class Vector3D {
friend ostream& operator<<(ostream& o, const Vector3D& v);
friend istream& operator>>(istream& i,Vector3D& v);
public:
Vector3D(float x, float y, float z) :
x_(x), y_(y), z_(z) {}
private:
float x_, y_, z_;
};
ostream& operator<<(ostream& o, const Vector3D& v)
{
o << "(" << v.x_ << ", " << v.y_ << ", " << v.z_ << ")" ;
return o;
}
istream& operator>>(istream& i,Vector3D& v)
{
char par, coma;
// formato: (X, Y, Z)
i >> par;
i >> v.x_;
i >> coma;
i >> v.y_;
i >> coma;
i >> v.z_;
return i;
}
Sin dependencias
El escenario ms simple es la serializacin de un objeto sin dependencias con el
resto, es decir, un objeto que no apunta a ningn otro y que est autocontenido.
La serializacin ser entonces trivial, y bastar con escribir cada una de los valores
que contenga, y recuperarlo en el mismo orden.
Sea la siguiente una interfaz para objetos que puedan serializarse.
Listado 22.6: Interfaz simple para objetos serializables
1 class ISerializable {
2 public:
3
virtual void read (std::istream& in) = 0;
4
virtual void write(std::ostream& out) = 0;
5 };
De este modo, todos los objetos que deriven de esta clase tendrn que implementar
la forma de escribir y leer de un stream. Es til delegar los detalles de serializacin al
objeto.
Supngase ahora una clase muy sencilla y sin dependencias, con un double, un
int y un string para serializar.
[641]
#include <iostream>
#include <sstream>
using namespace std;
template<typename T>
string toString(T in)
{
stringstream ss;
ss << in;
return ss.str();
}
template<typename T>
T toNumber(const string& s)
{
stringstream ss(s);
T t;
ss << s;
if (!(ss >> t))
throw;
return t;
}
int main(int argc, char *argv[])
{
stringstream s;
s << 98 << endl << "texto" << endl;
cout << (s.str() += "op\n") ;
string str = toString(9.001);
long
a = toNumber<long>("245345354525");
cout << a << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C22
[642]
void
ObjetoSimple::read(std::istream& in)
{
in.read((char*) &a_, sizeof(double));
in.read((char*) &b_, sizeof(int));
size_t len;
in.read((char*) &len, sizeof(size_t));
char* auxCad = new char[len+1];
in.read(auxCad, len);
auxCad[len] = \0;
cad_ = auxCad;
delete [] auxCad;
void
ObjetoSimple::write(std::ostream& out)
{
out.write((char*) &a_, sizeof(double));
out.write((char*) &b_, sizeof(int));
reservar la cantidad de memoria correcta al leerla de nuevo (8-9 ).
[643]
15
16
17
18
19
20
21
22 }
Al leer los objetos, poner en una tabla el puntero antiguo ledo, asociado a la
nueva direccin de memoria.
Hacer una pasada corrigiendo el valor de los punteros, buscando la correspondencia en la tabla.
Para ello necesitamos una interfaz nueva, que soporte la nueva funcin fixPtrs()
y otras dos para leer y recuperar la posicin de memoria del propio objeto.
Listado 22.10: Nueva interfaz de objeto serializable
1 class ISerializable {
2 public:
3
virtual void read (std::istream& in) = 0;
4
virtual void write(std::ostream& out) = 0;
5
6
virtual void fixPtrs () = 0;
7
C22
Almacenar todos los objetos, teniendo en cuenta que lo primero que se almacenar ser la direccin de memoria que ocupa el objeto actual. Los punteros del
mismo se almacenarn como el resto de datos.
[644]
8 protected:
9
virtual void readMemDir (std::istream& in)
10
virtual void writeMemDir(std::ostream& out)
11 };
= 0;
= 0;
En la lnea 15 se aade un puntero que almacenar la direccin de memoria de la
propia clase.
La implementacin de las funciones de lectura y escritura se muestra a continuacin.
Listado 22.12: Implementacin de la interfaz ISerializable (II)
1
2
3
4
5
6
7
8
9
10
11
[645]
11 private:
12
LookUpTable(){}
13
14
std::map<Serializable*, Serializable*> sMap_;
15
16 };
std::map<Serializable*, Serializable*>&
LookUpTable::getMap()
{
static std::auto_ptr<LookUpTable> instance_(new LookUpTable);
return instance_->sMap_;
}
Uno de los constructores ahora acepta un puntero a un objeto del mismo tipo. En
23 se declara un puntero a un objeto del mismo tipo, y tendr que ser serializado,
recuperado y arreglado. Con motivo de probar si la lectura ha sido correcta, se han
aadido un par de funciones, printCad, que imprime la cadena serializada del propio objeto y printOther, que imprime la cadena del objeto apuntado a travs del
primer mtodo.
De esto modo, la implementacin de la clase anterior sera la siguiente. Primero
para las funciones de impresin, que son las ms sencillas:
Listado 22.16: Denicin de ObjetoCompuesto
1 void ObjetoCompuesto::printCad()
2 {
3
std::cout << cad_ << std::endl;
4 }
5
C22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[646]
6 void ObjetoCompuesto::printOther()
7 {
8
if (obj_) obj_->printCad();
9 }
void
ObjetoCompuesto::read(std::istream& in)
{
readMemDir(in);
in.read((char*) &a_, sizeof(double));
in.read((char*) &b_, sizeof(int));
size_t len;
in.read((char*) &len, sizeof(size_t));
char* auxCad = new char[len+1];
in.read(auxCad, len);
auxCad[len] = \0;
cad_ = auxCad;
delete [] auxCad;
in.read((char*) &obj_, sizeof(ObjetoCompuesto*) );
std::cout
std::cout
std::cout
std::cout
std::cout
std::cout
<<
<<
<<
<<
<<
<<
"a_: "
<< a_
"b_: "
<< b_
"cad_: " << cad_
"obj_: " << obj_
"this: " << this
"--------------"
<<
<<
<<
<<
<<
<<
std::endl;
std::endl;
std::endl;
std::endl;
std::endl;
std::endl;
void
ObjetoCompuesto::write(std::ostream& out)
{
writeMemDir(out);
out.write((char*) &a_, sizeof(double));
out.write((char*) &b_, sizeof(int));
size_t len = cad_.length();
out.write((char*) &len, sizeof(size_t));
out.write((char*) cad_.c_str(), len);
[647]
3
4
5
6
7
8
9
10
11
12
13 }
return;
LookUpTable::itMapS it;
it = LookUpTable::getMap().find(obj_);
if (it == LookUpTable::getMap().end()) {
std::cout << "Puntero no encontrado" << std::endl;
throw;
}
obj_ = (ObjetoCompuesto*) it->second;
std::cout << "obj_ FIXED: " << obj_ << std::endl;
&p);
k->write(fout);
delete k;
ObjetoCompuesto r(10.2,
r.write(fout);
3243,
"2012", k);
}
cout << "\nRecuperando" << endl;
cout << "-----------" << endl;
ifstream fin("data.bin", ios_base::binary);
std::vector<Serializable*> objetosLeidos;
for (int i = 0; i < 5; ++i) {
ObjetoCompuesto* o = new ObjetoCompuesto();
o->read(fin);
objetosLeidos.push_back(o);
}
cout << "\nFix punteros" << endl;
cout << "------------" << endl;
for_each(objetosLeidos.begin(), objetosLeidos.end(),
mem_fun(&Serializable::fixPtrs));
cout << "\nProbando" << endl;
cout << "--------"
<< endl;
std::vector<Serializable*>::iterator it;
for (it = objetosLeidos.begin();
C22
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
[648]
46
it != objetosLeidos.end();
47
++it)
48
static_cast<ObjetoCompuesto*>((*it))->printOther();
49
50
return 0;
51 }
En las lneas 5-23 se crea el archivo que se usar como un stream y algunos
objetos que se van serializando. Algunos de ellos se crean en el stack y otro en el
heap. El archivo se cerrar puesto que la variable fout sale de contexto al terminar
el bloque.
En la lnea 27 se abre el mismo archivo para proceder a su lectura.
En 29-35 se
leen los datos del archivo y se van metiendo en un vector. En 40-41 se procede a
ejecutar la funcin fixPtrs de cada uno de los objetos almacenados dentro del vector. Justo despus se ejecutan las funciones que imprimen las cadenas de los objetos
apuntados, para comprobar que se han restaurado correctamente las dependencias.
La salida al ejecutar el programa anterior se muestra a continuacin:
Serializando
-----------* obj_: 0
* obj_: 0x7fff3f6dad80
* obj_: 0x7fff3f6dadb0
* obj_: 0x7fff3f6dadb0
* obj_: 0x11b3320
Recuperando
----------a_: 3.1371
b_: 1337
cad_: CEDV
obj_: 0
this: 0x11b3260
-------------a_: 9.235
b_: 31337
cad_: UCLM
obj_: 0x7fff3f6dad80
this: 0x11b3370
-------------a_: 9.235
b_: 6233
cad_: ESI
obj_: 0x7fff3f6dadb0
this: 0x11b3440
-------------a_: 300.2
b_: 1000
cad_: BRUE
obj_: 0x7fff3f6dadb0
this: 0x11b3520
-------------a_: 10.2
b_: 3243
cad_: 2012
obj_: 0x11b3320
this: 0x11b35d0
--------------
[649]
Fix punteros
-----------obj_ FIXED: 0x11b3260
obj_ FIXED: 0x11b3370
obj_ FIXED: 0x11b3370
obj_ FIXED: 0x11b3520
Probando
-------CEDV
UCLM
UCLM
BRUE
22.1.3.
Boost provee al programador de C++ con muchas utilidades, entre ellas la capacidad para serializar objetos de forma muy sencilla y metdica, convirtiendo una tarea
tediosa en un mero trmite.
Objetos sin dependencias
Para serializar la clase simple expuesta en la seccin anterior, primero habra del
siguiente modo:
Listado 22.20: Serializando un objeto simple con Boost
#include <fstream>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
class ObjetoSimple {
friend class boost::serialization::access;
public:
ObjetoSimple(double a, int b, std::string cad);
ObjetoSimple();
virtual ~ObjetoSimple();
void print();
template<class Archive>
void serialize(Archive & ar, const unsigned int version) {
ar & a_;
ar & b_;
ar & cad_;
}
private:
double
a_;
int
b_;
std::string cad_;
};
C22
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
[650]
En la lnea 8 se permite el acceso a esta clase desde la funcin access de Boost,
que se usar para la invocacin de serialize 18-22 . El smbolo & utilizado dentro
de dicha funcin templatizada representa a << o >> segn sea el tipo de Archive,
que ser el envoltorio de fstreams de Boost usado para la serializacin. Es precisamente en esa funcin donde se lleva a cabo la serializacin, puesto que para cada
variable de la clase, se procede a su lectura o escritura.
A continuacin se muestra cmo utilizar esta clase en un programa:
Listado 22.21: Uso de un objeto simple serializable con Boost
1
2
3
4
5
6
7
8
9
10
11
12
13
14
}
{
ObjetoSimple otherSimple;
ifstream fin("dataSimple", ios_base::binary );
boost::archive::text_iarchive inA(fin);
inA >> otherSimple;
otherSimple.print();
class ObjetoCompuesto
{
friend class boost::serialization::access;
public:
ObjetoCompuesto(double a, int b, std::string cad,
ObjetoCompuesto* other);
ObjetoCompuesto();
virtual ~ObjetoCompuesto();
void print();
void printOther();
template<class Archive>
void serialize(Archive & ar, const unsigned int version) {
ar & a_;
ar & b_;
ar & cad_;
ar & simple_;
ar & obj_;
}
private:
double
a_;
int
b_;
std::string cad_;
[651]
28
ObjetoSimple
simple_;
29
ObjetoCompuesto* obj_;
30 };
}
{
ObjetoCompuesto otherComp;
ObjetoCompuesto otherComp2;
ifstream fin("dataCompuesto", ios_base::binary );
boost::archive::text_iarchive inA(fin);
inA >> otherComp;
inA >> otherComp2;
otherComp.print();
cout << "\n\n\n";
otherComp2.print();
De hecho, dos de los pocos casos donde esta forma diere se muestran en el siguiente apartado.
Objetos derivados y con contenedores
C22
[652]
18 protected:
19
std::string baseName_;
20 };
Listado 22.25: Declarando un objeto derivado y con contenedores serializable con Boost
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
La nica cosa que hay que tener en cuenta a la hora de serializar este tipo de clases
es que hay que ser explcito a la hora de serializar
la parte relativa a la clase base. Esto
se lleva a cabo como se muestra en la lnea 20 del cdigo anterior.
Para que se puedan serializar contenedores, simplemente habr que incluir la cabecera de Boost correspondiente:
Listado 22.26: Cabecera de Boost para serializar vector
1 #include <boost/serialization/vector.hpp>
[653]
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
}
{
oD.print();
cout << "\n\n\n";
oD2.print();
cout << "\n\n\n";
Con todos los ejemplos anteriores se puede afrontar casi cualquier tipo de serializacin. Queda claro que el uso de Boost acelera el proceso, pero aun existen plataformas donde Boost no est portada (aquellas con compiladores que no soportan todas
las caractersticas de C++, por ejemplo) y donde la STL aun lucha por parecerse al
estndar. Es en stas donde habr que realizar una serializacin ms artesana y usar
algn tipo de tcnica parecida a la vista en las primeras secciones.
22.2.
C++ y scripting
A pesar de que el uso de un lenguaje de propsito general como C++ nos permite
abordar cualquier tipo de problema, existen lenguajes mas o menos apropiados para
tareas especcas. En el diseo de un lenguaje se tiene en mente aspectos como la
eciencia, portabilidad, simpleza, etc. y difcilmente se pueden alcanzar la excelencia
en todas las facetas.
Existen muchos proyectos que utilizan varios lenguajes de programacin, utilizando el mas apropiado para cada tarea. En esta seccin vamos a ver un ejemplo de
esta interaccin entre diversos lenguajes de programacin. En concreto vamos a coger
C++, como ya hemos comentado, un lenguaje orientado a objetos muy eciente en
su ejecucin y Python, un lenguaje interpretado (como java, php, Lua etc.) muy apropiado por su simpleza y portabilidad que nos permite desarrollar prototipos de forma
rpida y sencilla.
22.2.1.
Consideraciones de diseo
C22
No obstante, sera deseable que pudiramos realizar cada tarea en aquel lenguaje
mas apropiado para la tarea a realizar. Por ejemplo, mientras que C/C++ se caracterizan, entre otras cosas, por su eciencia, lenguajes como Python nos proporcionan un
entorno de programacin simple y muy productivo de cara a prototipado rpido as
como una gran portabilidad.
[654]
En el caso del desarrollo de juegos cuyo lenguaje principal sea de scripting (por
ejemplo, Python), una aproximacin genrica sera, desarrollar el juego por completo,
y despus, mediante tcnicas de proling se identican aquellas partes crticas para mejorar las prestaciones, que son las que se implementan en C/C++. Obviamente
aquellas partes que, a priori, ya sabemos que sus prestaciones son crticas podemos
anticiparnos y escribirlas directamente en C/C++.
En el caso de que la aplicacin se implemente en C/C++, utilizamos un lenguaje
de scripting para el uso de alguna librera concreta o para poder modicar/adaptar/extender/corregir el comportamiento sin tener que recompilar. En general, cuando
hablamos de C++ y scripting hablamos de utilizar las caractersticas de un lenguaje
de prototipado rpido desde C++, lo cual incluye, a grandes rasgos:
Crear y borrar objetos en el lenguaje de scripting e interaccionar con ellos invocando mtodos .
pasar datos y obtener resultados en invocaciones a funciones
Gestionar posibles errores que pudieran suceder en el proceso de interaccin,
incluyendo excepciones.
Otro ejemplo de las posibilidades de los lenguajes de scripting son utilizar lenguajes especcos ampliamente usados en otros entornos como la inteligencia articial,
para implementar las partes relacionadas del juego. Ejemplos de este tipo de lenguajes
seran LISP y Prolog ampliamente usados en inteligencia articial, y por lo tanto, muy
apropiados para modelar este tipo de problemas.
En la actualidad, las decisiones de diseo en cuanto a qu lenguaje de scripting
usar viene determinado por las caractersticas de dichos lenguajes. Sin tener en cuenta lenguajes muy orientados a problemas concretos como los mencionados LISP y
Prolog, y considerando slo aquellos lenguajes de scripting de propsito general, las
opciones actuales pasan por Lua y Python principalmente.
Atendiendo a sus caractersticas, Python:
Tiene una gran librera y, generalmente, bien documentada.
Facilita la gestin de cadenas y tiene operadores binarios.
A partir de la versin 2.4, Python tiene los denominados ctypes que permiten
acceder a tipos de libreras compartidas sin tener que hacer un wrapper C.
Tiene buenas prestaciones en computacin numrica (lo cual es muy deseable
en simuladores de eventos fsicos)
En contraste Lua es un lenguaje mas simple, originalmente pensado para labores
de conguracin y que ha sido orientado especcamente a la extensin de aplicaciones, algunas de sus caractersticas son:
En general, usa menos memoria y el intrprete es mas rpido que el de Python.
Tiene una sintaxis simple y fcil de aprender si bien es cierto que no tiene la
documentacin, ejemplos y tutoriales que Python.
Es cierto que tanto Lua como Python pueden ser utilizados para extender aplicaciones desarrolladas en C/C++, la decisin de qu lenguaje usar depende de qu
caractersticas queremos implementar en el lenguaje de scripting. Al ser Python un
lenguaje mas genrico, y por tanto mas verstil, que Lua ser el que estudiaremos mas
en profundidad.
Lua vs Python
Mientras que Lua est pensado para
extender aplicaciones y como lenguaje de conguracin, Python es
mas completo y puede ser utilizado
para funciones mas complejas.
[655]
22.2.2.
[:2]\n");
7
Py_Finalize();
8
return 0;
9 }
La funcin Py_Initialize() inicializa el intrprete creando la lista de mdulos cargados (sys.modules), crea los mdulos bsicos (__main__, __builtin__ y sys) y
crea la lista para la bsqueda de mdulos sys.path. En denitiva lo prepara para recibir
rdenes.PyRun_SimpleString() ejecuta un comando en el intrprete, podemos
ver que en este caso, importamos el mdulo sys y a continuacin imprimimos la
versin del intrprete que estamos ejecutando. Por ltimo, nalizamos la instancia
del intrprete liberando la memoria utilizada y destruyendo los objetos creados en la
sesin.
Todas estas funciones se denen en el archivo Python.h que proporciona la
instalacin de Python y que proporciona un API para acceder al entorno de ejecucin
de este lenguaje. El propio intrprete de Python utiliza esta librera.
Estas extensiones permiten invocar todo tipo de sentencias e interaccionar con
el intrprete de Python, eso si, de forma no muy orientada a objetos. Utilizando el
tipo PyObject (concretamente punteros a este tipo) podemos obtener referencias a
cualquier mdulo e invocar funciones en ellas. En la tabla 22.1 podemos ver, de forma
muy resumida, algunas funciones que nos pueden ser muy tiles. Por supuesto no estn
todas pero nos pueden dar una referencia para los pasos principales que necesitaramos
de cara a la interaccin C++ y Python.
La gestin de errores (del mdulo sys) en la actualidad est delegada en la funcin
exc_info()() que devuelve una terna que representan el tipo de excepcin que se
ha producido, su valor y la traza (lo que hasta la versin 1.5 representaban las variables
sys.exc_type, sys.exc_value y sys.exc_traceback).
Con el ejemplo visto en esta subseccin no existe un intercambio entre nuestro
programa C++ y el entorno Python. Por supuesto, el soporte nativo de Python nos
permite realizar cualquier forma de interaccin que necesitemos. No obstante, podemos beneciarnos de libreras que nos hacen esta interaccin mas natural y orientada
a objetos. Vamos a estudiar la interaccin entre ambos entornos mediante la librera
boost.
C22
[656]
Funcin
Cometido
Py_Initialize()
PyString_FromString(cadena)
Inicializa el intrprete
Retorna un puntero a PyObject con una cadena
(E.j. nombre del mdulo a cargar).
Carga un mdulo, retorna un puntero a PyObject.
Obtiene el diccionario con atributos y mtodos
del mdulo. Retorna un puntero a PyObject.
Obtiene una referencia a una funcin. Retorna
un puntero a PyObject
Llama a la funcin con los argumentos proporcionados.
Comprueba que es un objeto invocable.
Interpreta un archivo
Crea una tupla
Almacena un Item en una tupla
Imprime error.
Comprueba si PyObject es una lista
PyImport_Import(PyObject* name)
PyModule_GetDict(PyObject* modulo)
PyDict_GetItemString(PyObject *Diccionario, "funcin")
PyObject_CallObject(PyObject *funcin, argumentos)
PyCallable_Check(PyObject *funcion)
PyRun_File
PyTuple_New(items)
PyTuple_SetItem(tupla, posicin, item)
PyErr_Print()
PyList_Check(PyObject*)
22.2.3.
Librera boost
#include <boost/python.hpp>
#include <boost/python/import.hpp>
#include <iostream>
using namespace boost::python;
using namespace std;
int main(int argc, char *argv[])
{
[657]
10
11
12
13
14
15
16
17
18
19
20
21
22 }
Py_Initialize();
PyRun_SimpleString("import sys; major, minor = sys.version_info
[:2]");
object mainobj = import("__main__");
object dictionary = mainobj.attr("__dict__");
object major = dictionary["major"];
int major_version = extract<int>(major);
object minor = dictionary["minor"];
int minor_version = extract<int>(minor);
cout<<major_version<<"."<<minor_version<<endl;
Py_Finalize();
return 0;
C22
La plantilla extract() nos permite extraer de una instancia de object, en principio, cualquier tipo de C++. En nuestro ejemplo extraemos un entero correspondiente
a las versiones del intrprete de Python (versin mayor y menor). De forma genrica,
si no existe una conversin disponible para el tipo que le pasamos a extract(), una
excepcin Python (TypeError) es lanzada.
[658]
11
Py_Finalize();
12
return 0;
13 }
Adems se tiene un mtodo configure(), que nos permite obtener la conguracin del hroe en concreto, en este caso, simplemente la imprime. Bien asumimos
como decisin de diseo, que, salvo el nombre, el arma asignada a cada hroe ser
variable y podremos ir obteniendo diversas armas conforme avancemos en el juego.
Esta ltima parte la decidimos implementar en Python. Por lo tanto, habr un mtodo
en Python, al cual le pasaremos un objeto de la clase hero y ese mtodo lo congurar
de forma apropiada (en nuestro caso slo con el tipo de arma). En el siguiente listado
podemos ver esta funcin. En este ejemplo simplemente le pasa el arma (Kalasnikov)
invocando el mtodo correspondiente.
Listado 22.32: Congurar una instancia de la clase hero desde Python
1 def ConfHero(hero):
2
hero.weapon("Kalasnikov")
3
hero.configure()
[659]
En boost, se usa la macro BOOST_PYTHON_MODULE que bsicamente crea un
mdulo (ConfActors), que podremos usar en Python, deniendo las clases y mtodos
que le proporcionemos (en nuestro caso el constructor que acepta una cadena y los
mtodos configure() y weapon())
Listado 22.33: Exponer clases C++ a entornos Python
1
2
3
4
5
6
7
8
9
dictionary);
object ConfHero_function = dictionary["ConfHero"];
if(!ConfHero_function.is_none())
{
boost::shared_ptr<hero> Carpanta(new hero("Carpanta"));
ConfHero_function(ptr(Carpanta.get()));
hero *obj = ptr(Carpanta.get());
obj->configure();
}
Py_Finalize();
return 0;
C22
8
9
10
11
12
13
14
15
16
17
18 }
[660]
= [boost_python-py27])
5
6 setup (name = PackageName,
7
version = 1.0,
8
description = A C++ Package for python,
9
ext_modules = [module1])
Esto nos generar la librera especca para la mquina donde estamos y lo alojar en el directorio build que crear en el mismo directorio donde est el setup.py
(build/lib.linux-i686-2.7/ en nuestro caso) y con el nombre del mdulo (ConfActors.so) que le hemos indicado. A partir de este punto, previa importacin del mdulo ConfActors, podemos acceder a todas sus clases y mtodos directamente desde
el interprete de python como si fuera un mdulo mas escrito de forma nativa en este
lenguaje.
22.2.4.
Herramienta SWIG
[661]
Listado 22.36: Denicin de la clase Player
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <string>
#include <iostream>
class Player
{
std::string _name;
std::string _position;
public:
Player(std::string name);
void position(std::string pos);
void printConf();
};
Y su implementacin:
Listado 22.37: Implementacin de la clase en C++
1
2
3
4
5
6
7
8
9
10
11
12
13
import player
p = player.Player(Carpanta)
dir(player)
p.printConf()
p.position("Goalkeeper")
p.printConf()
C22
1 #define SWIG_FILE_WITH_INIT
2 #include "player.h"
3 %}
4
5 %include "std_string.i"
6 %include "player.h"
[662]
22.2.5.
Conclusiones
Captulo
23
Optimizacin
Francisco Moya Fernndez
ntes de entrar en materia vamos a matizar algunos conceptos. Optimizar hace referencia a obtener el mejor resultado posible. Pero la bondad o maldad
del resultado depende fuertemente de los criterios que se pretenden evaluar.
Por ejemplo, si queremos hacer un programa lo ms pequeo posible el resultado ser bastante diferente a si lo que queremos es el programa ms rpido posible. Por
tanto cuando hablamos de optimizacin debemos acompaar la frase con el objetivo,
con la magnitud que se pretende mejorar hasta el limite de lo posible. As se habla
frecuentemente de optimizar en velocidad u optimizar en tamao.
La optimizacin normalmente es un proceso iterativo e incremental. Cada etapa
produce un resultado mejor (o por lo menos ms fcil de mejorar). A cada una de
estas etapas del proceso se les suele denominar tambin optimizaciones, aunque sera
ms correcto hablar de etapas del proceso de optimizacin. Pero adems el objetivo
de optimizacin se enmarca en un contexto:
Las mismas optimizaciones que en una arquitectura concreta generan mejores
resultados pueden afectar negativamente al resultado en otras arquitecturas. Por
ejemplo, la asignacin de variables (o parmetros) a registros en un PowerPC
aprovecha el hecho de disponer de un buen nmero de registros de propsito
general. Si se usara el mismo algoritmo para asignar registros en un x86, en el
que la mayora de los registros son de propsito especco, obligara a introducir
multitud de instrucciones adicionales para almacenar temporalmente en la pila.
663
[664]
23.1.
Perlado de programas
[665]
Cul es la tasa de fallos de la memoria cach? Con el objetivo de mejorar la
localidad de la memoria.
Normalmente recabar este tipo de informacin implica instrumentar el cdigo aadiendo algunas instrucciones que permiten acumularla en un archivo (o varios) para
cada ejecucin del programa. La informacin de perlado es posteriormente analizada
con un programa, el perlador o proler.
Cada proler implementa el registro de la informacin de forma diferente. Bsicamente se utilizan cuatro tcnicas: trazas, muestreo estadstico, puntos de ruptura
hardware y contadores hardware. Veamos cada una de ellas en ms detalle:
Cuando el evento de inters corresponde a una operacin que requiere un tiempo
considerable es posible trazar cada ejecucin de la operacin sin un impacto
signicativo en las prestaciones del programa. sta es la tcnica empleada por
el perlador de Linux perf (descrito ms adelante) para trazar las operaciones
sobre el sistema de archivos, las operaciones de writeback, las operaciones de
gestin de energa, la recepcin y el manejo de interrupciones, las operaciones
de planicacin de procesos, etc. Tambin es la tcnica empleada por utilidades
como strace, que traza las llamadas al sistema de un proceso.
Sin embargo, en un programa de tamao considerable no es posible ejecutar
cdigo adicional en todos los eventos de inters (por ejemplo, en todas las llamadas a funcin). En ese caso se realiza un anlisis estadstico. Peridicamente
se realiza un muestreo del contador de programa y se analiza en qu funcin se
encuentra. Es ms, en lugar de solo observar el valor del contador de programa
puede analizar el contenido de la pila para determinar todos marcos de pila activos, es decir, la call trace. Con esto es posible determinar el grafo de llamadas
y el tiempo estimado destinado a cada funcin.
En lugar de instrumentar el cdigo o muestrear de forma estadstica, es posible utilizar los mecanismos previstos en los procesadores actuales para facilitar
el perlado. Por ejemplo, una posibilidad es el empleo de puntos de ruptura
hardware para detectar cundo se escribe una posicin de memoria, cundo se
escribe, o cundo se ejecuta la instruccin que contiene. Esta tcnica se puede emplear para trazar solo un conjunto limitado de funciones, o para estudiar
el patrn de accesos a un objeto. Tambin se emplea en la utilidad ltrace, que
traza las llamadas a procedimientos de bibliotecas dinmicas desde un proceso
determinado.
Por ltimo los procesadores modernos proporcionan otra funcionalidad especialmente interesante para el perlado. Disponen de una Performance Monitoring Unit que controla un conjunto de registros especiales denominados performance counters. Estos registros son capaces de contar determinados eventos,
tales como ciclos de la CPU, ciclos de bus, instrucciones, referencias a la cache,
fallos de la memoria cach, saltos o fallos en la prediccin de saltos. Estos registros pueden ser utilizados en prolers tales como perf para realizar mediciones
muy precisas.
Es importante conocer cundo se emplea cada una de estas tcnicas para poder
interpretar con precisin los datos del perlado. As, por ejemplo, las tcnicas basadas
en muestreo de la traza de llamadas debe entenderse en un contexto estadstico. Valores bajos en los contadores de llamadas no tienen signicado absoluto, sino en relacin
a otros contadores. Es muy posible que tengamos que ejecutar el mismo fragmento de
cdigo mltiples veces para eliminar cualquier sesgo estadstico.
C23
[666]
23.1.1.
A continuacin conviene congurar el kernel para que permita a los usuarios normales recabar estadsticas de todo tipo. Esto no debe hacerse con carcter general,
sino solo en las computadoras empleadas en el desarrollo, puesto que tambin facilita
la obtencin de informacin para realizar un ataque.
$ sudo sh -c "echo -1 > /proc/sys/kernel/perf_event_paranoid"
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
event]
event]
event]
event]
event]
event]
event]
event]
event]
[Software event]
[Software event]
[667]
page-faults OR faults
minor-faults
major-faults
context-switches OR cs
cpu-migrations OR migrations
alignment-faults
emulation-faults
rNNN (...)
mem:<addr>[:access]
i915:i915_gem_object_create
i915:i915_gem_object_bind
i915:i915_gem_object_unbind
...
sched:sched_wakeup
sched:sched_wakeup_new
sched:sched_switch
...
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
[Hardware
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
cache
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
event]
C23
L1-dcache-loads
L1-dcache-load-misses
L1-dcache-stores
L1-dcache-store-misses
L1-dcache-prefetches
L1-dcache-prefetch-misses
L1-icache-loads
L1-icache-load-misses
L1-icache-prefetches
L1-icache-prefetch-misses
LLC-loads
LLC-load-misses
LLC-stores
LLC-store-misses
LLC-prefetches
LLC-prefetch-misses
dTLB-loads
dTLB-load-misses
dTLB-stores
dTLB-store-misses
dTLB-prefetches
dTLB-prefetch-misses
iTLB-loads
iTLB-load-misses
branch-loads
branch-load-misses
node-loads
node-load-misses
node-stores
node-store-misses
node-prefetches
node-prefetch-misses
[Software
[Software
[Software
[Software
[Software
[Software
[Software
[668]
23.1.2.
Obteniendo ayuda
La primera suborden de perf que debe dominarse es help, que se emplea para
obtener ayuda. La ejecucin de perf help sin ms nos muestra todas las rdenes
disponibles. Las ms utilizadas son perf stat, perf record, perf report
y perf annotate.
Cada una de estas rdenes tienen ayuda especca que puede obtenerse con perf
help suborden.
23.1.3.
La operacin ms sencilla que se puede hacer con perf es contar eventos. Eso
puede realizarse con la suborden perf stat:
$ perf stat glxgears
Performance counter stats for glxgears:
80,416861
171
71
10732
109061681
75057377
58498153
68356682
task-clock
context-switches
CPU-migrations
page-faults
cycles
stalled-cycles-frontend
stalled-cycles-backend
instructions
14463080 branches
391522 branch-misses
#
0,069 CPUs utilized
#
0,002 M/sec
#
0,001 M/sec
#
0,133 M/sec
#
1,356 GHz
# 68,82 % frontend cycles idle
# 53,64 % backend cycles idle
#
0,63 insns per cycle
#
1,10 stalled cycles per insn
# 179,851 M/sec
#
2,71 % of all branches
[86,41 %]
[85,21 %]
[62,34 %]
[80,66 %]
[86,78 %]
[80,19 %]
[669]
Basta indicar el ejecutable a continuacin de perf stat. Por defecto muestra
un conjunto de mtricas comunes, que incluye eventos hardware (como los ciclos o las
instrucciones), eventos software (como los cambios de contexto), y mtricas derivadas
a la derecha (como el nmero de instrucciones por ciclo).
-e:
Puede utilizarse perf para medir un tipo de eventos concreto empleando la opcin
#
#
0,000 GHz
0,86 insns per cycle
Y podemos dividir entre los eventos que ocurren en espacio de usuario y los que
ocurren en espacio del kernel.
$ perf stat -e cycles:u,cycles:k precompute_landscape
Performance counter stats for precompute_landscape:
1827737 cycles:u
2612202 cycles:k
#
#
0,000 GHz
0,000 GHz
Todos los eventos hardware aceptan los modicadores u para ltrar solo los que
ocurren en espacio de usuario, k para ltrar los que ocurren en espacio del kernel y
uk para contabilizar ambos de forma explcita. Hay otros modicadores disponibles,
incluso alguno dependiente del procesador en el que se ejecuta.
23.1.4.
Multiplexacin y escalado
cycles
cycles
cycles
cycles
#
#
#
#
0,000
0,000
0,000
0,000
GHz
GHz
GHz
GHz
3 Lo ms normal es disponer de dos o cuatro contadores genricos y otros tantos especcos. Realiza la
misma prueba en tu ordenador para comprobar cuntos contadores genricos tiene.
C23
Las PMU tienen dos tipos de contadores: los contadores jos, que cuentan un nico
tipo de evento, y los contadores genricos, que pueden congurarse para contar cualquier evento hardware. Cuando el usuario solicita ms eventos de los que fsicamente
se pueden contar con los contadores implementados el sistema de perlado multiplexa los contadores disponibles. Esto hace que parte del tiempo se estn contando unos
eventos y parte del tiempo se estn contando otros eventos distintos.
[670]
Puede verse que la precisin es absoluta, los cuatro contadores han contado prcticamente la misma cantidad de ciclos. En cambio, veamos qu pasa cuando se solicitan
5 eventos idnticos:
$ perf stat -e cycles,cycles,cycles,cycles,cycles render_frame
Performance counter stats for render_frame:
801863997
801685466
792515645
792876560
793921257
cycles
cycles
cycles
cycles
cycles
#
#
#
#
#
0,000
0,000
0,000
0,000
0,000
GHz
GHz
GHz
GHz
GHz
[79,06 %]
[80,14 %]
[80,37 %]
[80,37 %]
[80,08 %]
Los valores son signicativamente diferentes, pero los porcentajes entre corchetes
nos previenen de que se ha realizado un escalado. Por ejemplo, el primer contador ha
estado contabilizando ciclos durante el 79,06 % del tiempo. El valor obtenido en el
contador se ha escalado dividiendo por 0,7906 para obtener el valor mostrado.
En este caso los contadores nos dan una aproximacin, no un valor completamente
able. Nos vale para evaluar mejoras en porcentajes signicativos, pero no mejoras
de un 1 %, porque como vemos el escalado ya introduce un error de esa magnitud.
Adems en algunas mediciones el resultado depender del momento concreto en que
se evalen o de la carga del sistema en el momento de la medida. Para suavizar todos
estos efectos estadsticos se puede ejecutar varias veces empleando la opcin -r.
$ perf stat -r 10 -e cycles,cycles,cycles,cycles,cycles render_frame
Performance counter stats for render_frame (10 runs):
803926738
804290331
802303057
797518018
799832288
cycles
cycles
cycles
cycles
cycles
#
#
#
#
#
0,000
0,000
0,000
0,000
0,000
GHz
GHz
GHz
GHz
GHz
(
(
(
(
(
+++++-
0,15 %
0,14 %
0,17 %
0,11 %
0,19 %
)
)
)
)
)
[79,42 %]
[79,66 %]
[80,21 %]
[80,59 %]
[80,15 %]
0,39 % )
23.1.5.
Es posible contabilizar los eventos solo en un hilo, o en todos los hilos de un proceso, o en todos los procesos de una CPU, o de un conjunto de ellas. Por defecto perf
contabiliza eventos del hilo principal incluyendo todos los subprocesos, creados con
fork(), o hilos, creados con pthread_create(), lanzados durante la ejecucin.
Este comportamiento se implementa con un mecanismo de herencia de contadores que
puede desactivarse con la opcin -i de perf stat.
Alternativamente se puede recolectar datos de un conjunto de procesadores en
lugar de un proceso concreto. Este modo se activa con la opcin -a y opcionalmente
complementado con la opcin -C. Al utilizar la opcin -a se activa la recoleccin
de datos por CPU, pero por defecto se agregan todos los contadores de todas las CPU
[671]
(recoleccin de datos a nivel de sistema). Con la opcin -C podemos seleccionar
la CPU o conjunto de CPUs de los que se recaban estadsticas. Por ejemplo, para
recolectar el nmero de fallos de pgina en espacio de usuario de las CPUs 0 y 2
durante 5 segundos:
$ perf stat -a -e faults -C 0,2 sleep 5
Performance counter stats for sleep 5:
233 faults
5,001227158 seconds time elapsed
Ntese que utilizamos la orden sleep para no consumir ciclos y de esta forma
no inuir en la medida.
23.1.6.
Muestreo de eventos
Adems de contar eventos, perf puede realizar un muestreo similar a otros prolers. En este caso perf record recolecta datos en un archivo llamado perf.data
que posteriormente puede analizarse con perf report o perf annotate.
El periodo de muestreo se especica en nmero de eventos. Si el evento que se
utiliza para el muestreo es cycles (es as por defecto) entonces el periodo tiene
relacin directa con el tiempo, pero en el caso general no tiene por qu. Incluso en el
caso por defecto la relacin con el tiempo no es lineal, en caso de que el procesador
tenga activos modos de escalado de frecuencia.
Por defecto perf record registra 1000 muestras por segundo y ajusta dinmicamente el periodo para que mantenga esta tasa. El usuario puede establecer una
frecuencia de muestreo utilizando la opcin -F o puede establecer un periodo jo con
la opcin -c.
A diferencia de otros prolers, perf record puede recoger estadsticas a nivel
del sistema completo o de un conjunto de CPUs concreto empleando las opciones -a
y -C que ya hemos visto al explicar perf stat.
C23
[672]
Para ilustrar la mecnica veremos un caso real. Ingo Molnar, uno de los principales
desarrolladores de la infraestructura de perlado de Linux, tiene multitud de mensajes
en diversos foros sobre optimizaciones concretas que fueron primero identicadas
mediante el uso de perf. Uno de ellos4 describe una optimizacin signicativa de
git, el sistema de control de versiones.
En primer lugar realiza una fase de anlisis de una operacin concreta que revela
un dato intranquilizador. Al utilizar la operacin de compactacin git gc descubre
un nmero elevado de ciclos de estancamiento (stalled cycles5 ):
$ perf record -e stalled-cycles -F 10000 ./git gc
$ perf report --stdio
# Events: 26K stalled-cycles
#
# Overhead Command
Shared Object
# ........ .......... .....................
#
26.07 %
git git
Symbol
.......................
[.] lookup_object
4 http://thread.gmane.org/gmane.comp.version-control.git/172286
[673]
10.22 %
7.08 %
6.63 %
5.37 %
4.03 %
3.09 %
2.81 %
git
git
git
git
git
git
git
libz.so.1.2.5
libz.so.1.2.5
git
[kernel.kallsyms]
git
libc-2.13.90.so
libc-2.13.90.so
[.]
[.]
[.]
[k]
[.]
[.]
[.]
0xc43a
inflate
find_pack_entry_one
do_raw_spin_lock
lookup_blob
__strlen_sse42
__memcpy_ssse3_back
Ingo descubre que la funcin find_pack_entry_one() se lleva un porcentaje signicativo de los ciclos de estancamiento. Por tanto examina el contenido de esa
funcin con perf annotate. Para poder extraer todo el benecio de esta orden es
interesante compilar el programa con informacin de depuracin.
$ perf annotate find_pack_entry_one
Percent | Source code & Disassembly of git
--------------------------------------------:
...
:
int cmp = hashcmp(index + mi * stride, sha1);
0.90 : 4b9264: 89 ee
mov
%ebp, %esi
0.45 : 4b9266: 41 0f af f2
imul
%r10d, %esi
2.86 : 4b926a: 4c 01 de
add
%r11, %rsi
53.34 : 4b926d: f3 a6
repz cmpsb %es:( %rdi), %ds:( %rsi)
14.37 : 4b926f: 0f 92 c0
setb
%al
5.78 : 4b9272: 41 0f 97 c4
seta
%r12b
1.52 : 4b9276: 41 28 c4
sub
%al, %r12b
2771.119892
1,813
167
39,210
8,828,405,654
2,102,083,909
8,821,931,740
task-clock
context-switches
CPU-migrations
page-faults
cycles
stalled-cycles
instructions
1,750,408,175 branches
74,612,120 branch-misses
3.211098537
#
#
#
#
#
#
#
#
#
#
0.863
0.001
0.000
0.014
3.186
23.81 %
1.00
0.24
631.661
4.26 %
( +-
CPUs utilized
M/sec
M/sec
M/sec
GHz
of all cycles are idle
insns per cycle
stalled cycles per insn
M/sec
of all branches
(
(
(
(
(
(
++++++-
( +( +( +-
0.16 %
3.06 %
2.92 %
0.26 %
0.13 %
0.52 %
)
)
)
)
)
)
0.04 % )
0.04 % )
0.07 % )
1.52 % )
La opcin -sync hace que se ejecute una llamada sync() (vuelca los buffers
pendientes de escritura de los sistemas de archivos) antes de cada ejecucin para reducir el ruido en el tiempo transcurrido.
Despus de la optimizacin el resultado es:
$ perf stat --sync --repeat 10 ./git gc
Performance counter stats for ./git gc (10 runs):
2349.498022 task-clock
( +-
0.15 % )
C23
[674]
1,842
164
39,350
7,484,317,230
1,577,673,341
11,067,826,786
2,489,157,909 branches
59,384,019 branch-misses
2.910829134
#
0.001 M/sec
#
0.000 M/sec
#
0.017 M/sec
#
3.185 GHz
#
21.08 % of all cycles are idle
#
1.48 insns per cycle
#
0.14 stalled cycles per insn
# 1059.442 M/sec
#
2.39 % of all branches
( +-
(
(
(
(
(
+++++-
( +( +( +-
2.50 %
3.67 %
0.06 %
0.15 %
0.67 %
)
)
)
)
)
0.02 % )
0.02 % )
0.22 % )
1.39 % )
branch-misses
74,612,120
59,384,019
% del total
4.26 % ( 0.07 % )
2.39 % ( 0.22 % )
Por alguna razn el bucle abierto es ms sencillo de predecir por parte de la CPU
por lo que produce menos errores de prediccin.
No obstante es importante entender que estas optimizaciones corresponden a problemas en otros puntos (compilador que genera cdigo subptimo, y arquitectura que
privilegia un estilo frente a otro). Por tanto se trata de optimizaciones con fecha de
caducidad. Cuando se utilice una versin ms reciente de GCC u otro compilador ms
agresivo en las optimizaciones esta optimizacin no tendr sentido.
Un caso clebre similar fue la optimizacin del recorrido de listas en el kernel
Linux6 . Las listas son estructuras muy poco adecuadas para la memoria cach. Al
no tener los elementos contiguos generan innumerables fallos de cach. Mientras se
produce un fallo de cach el procesador est parcialmente parado puesto que necesita
el dato de la memoria para operar. Por esta razn en Linux se emple una optimizacin
denominada prefetching. Antes de operar con un elemento se accede al siguiente. De
esta forma mientras est operando con el elemento es posible ir transriendo los datos
de la memoria a la cach.
Desgraciadamente los procesadores modernos incorporan sus propias unidades de
prefetch que realizan un trabajo mucho mejor que el manual, puesto que no interere
con el TLB. El propio Ingo Molnar reporta que esta optimizacin estaba realmente
causando un impacto de 0,5 %.
La leccin que debemos aprender es que nunca se debe optimizar sin medir, que
las optimizaciones dependen del entorno de ejecucin, y que si el entorno de ejecucin
vara las optimizaciones deben re-evaluarse.
6 https://lwn.net/Articles/444336/
[675]
Paquete
Valgrind Callgrind7
Google Performance Tools8
Herramienta
kCacheGrind
google-pprof
GNU Proler9
gprof
nvvp
sprofile
Descripcin
Excelentes capacidades de representacin grca.
Permite perlado de CPU y de memoria dinmica. Permite salida en formato callgrind para poder analizar con
kCacheGrind.
Es una herramienta estndar pero ha ido perdiendo su utilidad conforme fueron surgiendo los perladores basados
en PMU.
Es especco para GPUs nVidia.
Es especco para GPUs AMD/ATI Radeon.
23.1.7.
23.1.8.
Otros perladores
C23
[676]
Esto genera el archivo prof.data con los datos de perlado, que luego pueden
examinarse con google-pprof. Entre otras capacidades permite representacin
grca del grafo de llamadas o compatibilidad con el formato de kcachegrind.
Una caracterstica interesante de Google Performance Tools es la capacidad de
realizar el perlado solo para una seccin concreta del cdigo. Para ello, en lugar de
denir la variable CPUPROFILE basta incluir en el cdigo llamadas a las funciones
ProfilerStart() y ProfilerStop().
Para un desarrollador de videojuegos es destacable la aparicin de perladores
especcos para GPUs. Las propias GPUs tienen una PMU (Performance Monitoring
Unit) que permite recabar informacin de contadores especcos. De momento en el
mundo del software libre han emergido nVidia Visual Proler, AMD APP Proler
y extensiones de Intel a perf para utilizar los contadores de la GPU (perf gpu).
Probablemente en un futuro cercano veremos estas extensiones incorporadas en la
distribucin ocial de linux-tools.
GPU Prolers
De momento solo
porciona un proler
dades grcas sobre
AMD APP Proler
GNU/Linux pero no
grca.
23.2.
[677]
C fue diseado con el objetivo inicial de programar un sistema operativo. Por este
motivo, desde las primeras versiones incorpora caractersticas de muy bajo nivel que
permite dirigir al compilador para generar cdigo ms eciente. Variables registro,
funciones en lnea, paso por referencia, o plantillas son algunas de las caractersticas
que nos permiten indicar al compilador cundo debe esforzarse en buscar la opcin
ms rpida. Sin embargo, la mayora de las construcciones son simplemente indicaciones o sugerencias, que el compilador puede ignorar libremente si encuentra una
solucin mejor. En la actualidad tenemos compiladores libres maduros con capacidades comparables a los mejores compiladores comerciales, por lo que frecuentemente
las indicaciones del programador son ignoradas.
23.2.1.
Variables registro
Los ms viejos del lugar recordarn un modicador opcional para las variables
denominado register. Este modicador indica al compilador que se trata de una
variable especialmente crtica, por lo que sugiere almacenarla en un registro del procesador. Era frecuente ver cdigo como ste:
C23
[678]
Listado 23.1: Utilizacin arcaica de register para sumar los 1000 primeros nmeros naturales.
1 register unsigned i, sum = 0;
2 for (i=1; i<1000; ++i)
3
sum += i;
Esta palabra clave est en desuso porque los algoritmos de asignacin de registros
actuales son mucho mejores que la intuicin humana. Pero adems, aunque se utilizara, sera totalmente ignorada por el compilador. La mera aparicin de register en
un programa debe ser considerada como un bug, porque engaa al lector del programa
hacindole creer que dicha variable ser asignada a un registro, cuando ese aspecto
est fuera del control del programador.
23.2.2.
El resultado es el siguiente:
Listado 23.3: Resultado de la compilacin del ejemplo anterior.
1
.file
"inl.cc"
2
.text
3
.p2align 4,,15
4
.globl _Z3sumPij
5
.type
_Z3sumPij, @function
6 _Z3sumPij:
7 .LFB0:
8
.cfi_startproc
9
xorl
%eax, %eax
10
testl
%esi, %esi
11
pushq
%rbx
12
.cfi_def_cfa_offset 16
13
.cfi_offset 3, -16
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
[679]
je .L2
movq
%rdi, %r8
movq
%rdi, %rcx
andl
$15, %r8d
shrq
$2, %r8
negq
%r8
andl
$3, %r8d
cmpl
%esi, %r8d
cmova
%esi, %r8d
xorl
%edx, %edx
testl
%r8d, %r8d
movl
%r8d, %ebx
je .L11
.p2align 4,,10
.p2align 3
.L4:
addl
$1, %edx
addl
( %rcx), %eax
addq
$4, %rcx
cmpl
%r8d, %edx
jb .L4
cmpl
%r8d, %esi
je .L2
.L3:
movl
%esi, %r11d
subl
%r8d, %r11d
movl
%r11d, %r9d
shrl
$2, %r9d
leal
0(, %r9,4), %r10d
testl
%r10d, %r10d
je .L6
pxor
%xmm0, %xmm0
leaq
( %rdi, %rbx,4), %r8
xorl
%ecx, %ecx
.p2align 4,,10
.p2align 3
.L7:
addl
$1, %ecx
paddd
( %r8), %xmm0
addq
$16, %r8
cmpl
%r9d, %ecx
jb .L7
movdqa %xmm0, %xmm1
addl
%r10d, %edx
psrldq $8, %xmm1
paddd
%xmm1, %xmm0
movdqa %xmm0, %xmm1
psrldq $4, %xmm1
paddd
%xmm1, %xmm0
movd
%xmm0, -4( %rsp)
addl
-4( %rsp), %eax
cmpl
%r10d, %r11d
je .L2
.L6:
movslq %edx, %rcx
leaq
( %rdi, %rcx,4), %rcx
.p2align 4,,10
.p2align 3
.L9:
addl
$1, %edx
addl
( %rcx), %eax
addq
$4, %rcx
cmpl
%edx, %esi
ja .L9
.L2:
popq
%rbx
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L11:
.cfi_restore_state
C23
[680]
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
movl
%r8d, %eax
jmp .L3
.cfi_endproc
.LFE0:
.size
_Z3sumPij, .-_Z3sumPij
.section
.text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type
main, @function
main:
.LFB1:
.cfi_startproc
movl
$15, %eax
ret
.cfi_endproc
.LFE1:
.size
main, .-main
.ident "GCC: (Debian 4.6.3-1) 4.6.3"
.section
.note.GNU-stack,"",@progbits
movl
ret
$15, %eax
#include <iostream>
using namespace std;
int sum(int* a, unsigned sz);
struct A {
A() {
int a[] = { 1, 1, 1, 1 };
cout << sum(a, 4) << endl;
}
};
A a;
[681]
No ha quedado ningn smbolo reconocible. El montador ha optimizado el ejecutable para que solo contenga los smbolos utilizados. Y si un plugin necesita la funcin
sum()? La respuesta la conocemos, aunque no conocamos los detalles, basta montar
con la opcin -rdynamic:
$ g++ -O3 -rdynamic -o inl inl.cc
$ nm --dynamic inl
00000000004008d8 R _IO_stdin_used
w _Jv_RegisterClasses
0000000000400720 T _Z3sumPij
0000000000600c00 A __bss_start
0000000000600bf0 D __data_start
w __gmon_start__
00000000004007f0 T __libc_csu_fini
0000000000400800 T __libc_csu_init
U __libc_start_main
0000000000600c00 A _edata
0000000000600c10 A _end
00000000004008c8 T _fini
00000000004005f8 T _init
0000000000400638 T _start
0000000000600bf0 W data_start
0000000000400630 T main
C23
[682]
#include <string.h>
static inline int hashcmp(const char *sha1, const char *sha2)
{
return memcmp(sha1, sha2, 20);
}
extern const char null_sha1[20] __attribute__((aligned(8)));
static inline int is_null_sha1(const char *sha1)
{
return !hashcmp(sha1, null_sha1);
}
int ejemplo(char* sha1, char* index, unsigned mi)
{
int cmp, i;
for (i=0; i<mi; ++i) {
cmp = hashcmp(index + i * 1024, sha1);
if (cmp == 0) return 0;
}
return cmp;
}
Estas funciones, que eran expandidas en lnea por el compilador, exhiban un comportamiento anmalo con respecto a los ciclos de estancamiento y a la prediccin de
saltos. Por lo que Ingo propone la siguiente optimizacin:
Listado 23.8: Optimizacin de funciones crticas en la ejecucin de git gc.
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
return 0;
[683]
Ciclos
192458
197163
189164
Ciclos Opt.
190022
198232
191826
Mejora
1,3 %
-0,5 %
-1,4 %
Cuadro 23.3: Resultados de la optimizacin de Ingo Molnar con compiladores actuales (100 repeticiones).
23.2.3.
Eliminacin de copias
Cuando se cumplen determinados criterios una implementacin puede omitir la llamada al constructor de copia o movimiento de un objeto, incluso cuando el constructor y/o destructor de dicho objeto tienen efectos
de lado. En estos casos la implementacin simplemente trata la fuente y el
destino de la operacin de copia o movimiento omitida como dos formas
diferentes de referirse al mismo objeto, y la destruccin de dicho objeto
ocurre cuando ambos objetos hubieran sido destruidos sin la optimizacin. Esta elisin de las operaciones de copia o movimiento, denominada
elisin de copia, se permite en las siguientes circunstancias (que pueden
ser combinadas para eliminar copias mltiples):
En una sentencia return de una funcin cuyo tipo de retorno sea
una clase, cuando la expresin es el nombre de un objeto automtico no voltil (que no sea un parmetro de funcin o un parmetro de
una clusula catch) con el mismo tipo de retorno de la funcin (que
no puede ser const ni volatile), la operacin de copia o movimiento
puede ser omitida mediante la construccin directa del objeto automtico en el propio valor de retorno de la funcin.
En una expresin throw, cuando el operando es el nombre de un objeto automtico no voltil (que no sea un parmetro de funcin o un
parmetro de una clusula catch) cuyo mbito de declaracin no se
extienda ms all del nal del bloque try ms interior que contenga a
C23
En la mayor parte del estndar de C++ se suele indicar que el compilador tiene
libertad para optimizar siempre que el resultado se comporte como si esas optimizaciones no hubieran tenido lugar. Sin embargo el estndar permite adems un rango
de optimizaciones muy concreto pero con gran impacto en prestaciones, que pueden
cambiar el comportamiento de un programa. En [49], seccin 12.8, 32 introduce la
nocin de copy elision. Lo que sigue es una traduccin literal del estndar.
[684]
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
Aqu los criterios de elisin pueden combinarse para eliminar dos llamadas al constructor de copia de Thing: la copia del objeto automtico
local t en el objeto temporal para el valor de retorno de la funcin f() y
la copia de ese objeto temporal al objeto t2. Por tanto la construccin del
objeto local t puede verse como la inicializacin directa del objeto t2, y
la destruccin de dicho objeto tendr lugar al terminar el programa. Aadir un constructor de movimiento a Thing tiene el mismo efecto, en cuyo
caso es el constructor de movimiento del objeto temporal a t2 el que se
elide.
Copy elision es un concepto que incluye dos optimizaciones frecuentes en compiladores de C++: RVO (tercera circunstancia contemplada en el estndar) y NRVO (Named Return Value Optimization) (primera circunstancia contemplada en el estndar).
23.2.4.
Volatile
23.3. Conclusiones
[685]
El programador piensa que el bucle implementa un retardo y por tanto la funcin
permite generar un pulso en el bit menos signicativo. Compilando el ejemplo con
mximo nivel de optimizacin obtenemos lo siguiente:
1 _Z5resetRj:
2
movl
3
ret
$0, ( %rdi)
El compilador ha eliminado todo hasta el punto de que ni siquiera escribe el pulso. Una forma sencilla de corregir este comportamiento es declarar el contador i y
el registro reg como volatile. Esto indica al compilador que no debe hacer optimizaciones con respecto a esas variables. Otra forma sera sustituir el bucle de espera
por una llamada a funcin (por ejemplo usleep(10)).
23.3.
Conclusiones
C23
Por otro lado el programador de videojuegos deber optimizar para una plataforma o conjunto de plataformas que se identican como objetivo. Algunas de las optimizaciones sern especcas para estas plataformas y debern re-evaluarse cuando el
entorno cambie.
Captulo
24
Validacin y Pruebas
David Villa Alises
a programacin, igual que cualquier otra disciplina tcnica, debera ofrecer garantas sobre los resultados. La formalizacin de algoritmos ofrece garantas
indiscutibles y se utiliza con xito en el mbito de los algoritmos numricos y
lgicos. Sin embargo, el desarrollo de software en muchos mbitos est fuertemente
ligado a requisitos que provienen directamente de necesidades de un cliente. En la
mayora de los casos, esas necesidades no se pueden formalizar dado que el cliente
expresa habitualmente requisitos ambiguos o incluso contradictorios. El desarrollo de
software no puede ser ajeno a esa realidad y debe integrar al cliente de forma que
pueda ayudar a validar, renar o recticar la funcionalidad del sistema durante todo el
proceso.
Un programador responsable comprueba que su software satisface los requisitos
del cliente, comprueba los casos tpicos y se asegura que los errores detectados (ya
sea durante el desarrollo o en produccin) se resuelven y no vuelven a aparecer. Es
imposible escribir software perfecto (a da de hoy) pero un programador realmente
profesional escribe cdigo limpio, legible, fcil de modicar y adaptar a nuevas necesidades. En este captulo veremos algunas tcnicas que pueden ayudar a escribir
cdigo ms limpio y robusto.
24.1.
Programacin defensiva
La expresin programacin defensiva se reere a las tcnicas que ayudan al programador a evitar, localizar y depurar fallos, especialmente aquellos que se producen
en tiempo de ejecucin. En muchas situaciones, especialmente con lenguajes como
C y C++, el programa puede realizar una operacin ilegal que puede terminar con la
ejecucin del proceso por parte del sistema operativo. El caso ms conocido en este
687
[688]
Normalmente el programador aade comprobaciones que validan los datos de entrada procedentes de la interfaz con el usuario. Se trata principalmente de convertir y
vericar que los valores obtenidos se encuentran dentro de los rangos o tengan valores
segn lo esperado. Si no es as, se informa mediante la interfaz de usuario que corresponda. Sin embargo, cuando se escribe una funcin que va a ser invocada desde otra
parte, no se realiza una validacin previa de los datos de entrada ni tampoco de los
producidos por la funcin. En condiciones normales podemos asumir que la funcin
va a ser invocada con los valores correctos, pero ocurre que un error en la lgica del
programa o un simple error-por-uno puede implicar que utilicemos incorrectamente
nuestras propias funciones, provocando errores difciles de localizar.
La herramienta ms simple, a la vez que potente, para denir invariantes, precondiciones o post-condiciones es la funcin assert()1 , que forma parte de la librera estndar de prcticamente todos los lenguajes modernos. assert() sirve, tal
como indica su nombre, para denir aserciones, que en el caso de C++ ser toda expresin que pueda ser evaluada como cierta. El siguiente listado es un ejemplo mnimo
de usa asercin. Se muestra tambin el resultado de ejecutar el programa cuando la
asercin falla:
Error por uno
Listado 24.2: assert-argc.cc: Un ejemplo sencillo de assert()
1 #include <cassert>
2
3 int main(int argc, char *argv[]) {
4
assert(argc == 2);
5
return 0;
6 }
1 En
[689]
$ ./assert-argc hello
$ ./assert-argc
assert-argc: assert-argc.cc:4: int main(int, char**): Assertion argc
== 2 failed.
Abortado
24.1.1.
Sobrecarga
[...]
#ifndef NDEBUG
vector<int> values = get_values();
assert(values.size());
#endif
2 Por contra, algunos autores como Tony Hoare, deenden que en la versin de produccin es dnde ms
necesarias son las aserciones.
C24
[690]
Denir la constante en el cdigo, aparte de ser incmodo cuando se necesita activar/desactivar con frecuencia, puede ser confuso porque podra haber cheros que se
preprocesan antes de que la constante sea denida.
24.2.
Desarrollo gil
24.3.
TDD
24.3. TDD
[691]
24.3.1.
Con TDD la prueba es el primer paso que desencadena todo el proceso de desarrollo. En este sentido, las pruebas no son una mera herramienta de testing. Las pruebas
se utilizan como un medio para capturar y denir con detalle los requisitos del usuario,
pero tambin como ayuda para obtener un diseo consistente evitando aadir complejidad innecesaria. Hacer un desarrollo dirigido por pruebas acota el trabajo a realizar:
si todas las pruebas pasan, el programa est terminado, algo que puede no resultar
trivial con otros modelos de desarrollo.
Este proceso resulta muy til para evitar malgastar tiempo y esfuerzo aadiendo funcionalidad que en realidad no se ha solicitado. Este concepto se conoce como
YAGNI (You Aint Gonna Need It) y aunque a primera vista pueda parecer una cuestin trivial, si se analiza detenidamente, puede suponer un gran impacto en cualquier
proyecto. Es frecuente que los programadores entusiastas y motivados por la tarea acaben generando un diseo complejo plasmado en una gran cantidad de cdigo difcil
de mantener, mejorar y reparar.
24.3.2.
Cada uno de los requisitos identicados debe ser analizado hasta obtener una serie
de escenarios que puedan ser probados de forma independiente. Cada uno de esos
escenarios se convertir en una prueba. Para cada uno de ellos:
Escribe la prueba haciendo uso de las interfaces del sistema (es probable que
an no existan!) y ejectala. La prueba debera fallar y debes comprobar que es
as (rojo).
A continuacin escribe el cdigo de produccin mnimo necesario para que la
prueba pase (verde). Ese cdigo mnimo debe ser solo el imprescindible, lo
ms simple posible, hasta el extremo de escribir mtodos que simplemente retornan el valor que la prueba espera3 . Eso ayuda a validar la interfaz y conrma
que la prueba est bien especicada. Pruebas posteriores probablemente obligarn a modicar el cdigo de produccin para que pueda considerar todas las
posibles situaciones. A esto se le llama triangulacin y es la base de TDD:
Las pruebas dirigen el diseo.
Este sencillo mtodo de trabajo (el algoritmo TDD) favorece que los programadores se concentren en lo que realmente importa: satisfacer los requisitos del usuario.
Tambin ayuda al personal con poca experiencia en el proyecto a decidir cul es el
prximo paso en lugar de divagar o tratando de mejorar el programa aadiendo
funcionalidades no solicitadas.
3 Kent
C24
[692]
24.4.
Tipos de pruebas
Hay muchas formas de clasicar las pruebas, y todo lo referente al testing tradicional es aplicable aqu, aunque quiz de un modo diferente: pruebas de caja negra y
blanca, pruebas de aceptacin, integracin, sistema, unitarias y largo etctera. En el
contexto de las metodologas giles podemos concretar los siguientes tipos de pruebas [13]:
De aceptacin Idealmente debera estar especicado por el cliente o al menos por
un analista con la ayuda del cliente. Se expresa en trminos del dominio de la
aplicacin, sin detalles de implementacin. Esto incluye los test no funcionales,
es decir, aquellos que expresan requisitos no relacionados con los resultados
obtenidos sino sobre cuestiones como tiempo de ejecucin, consumo de energa,
etc.
De sistema Es un test que utiliza el sistema completo, desde el interfaz de usuario
hasta la base de datos, y lo hace del mismo modo que lo haran los usuarios
reales. Son pruebas muy frgiles, es decir, pequeos cambios sin relacin aparente pueden hacer fallar la prueba aunque funcionalmente el sistema sea correcto.
Unitarios Se utilizan para probar un nico componente del sistema: un mtodo o funcin, y para unas condiciones concretas (un escenario). La validacin se puede
hacer bien comprobando el estado nal conocido el estado inicial o por la interaccin entre el componente que se est probando y sus colaboradores.
Desde el punto de vista gil hay una pauta clara: las pruebas se escriben para ejecutarse, y debera ocurrir tan a menudo como sea posible. Lo ideal sera ejecutar todas
las pruebas despus de cada cambio en cualquier parte de la aplicacin. Obviamente
eso resulta prohibitivo incluso para aplicaciones pequeas. Hay que llegar a una solucin de compromiso. Por este motivo, las pruebas unitarias son las ms importantes.
Si estn bien escritas, las pruebas unitarias se deberan poder ejecutar en muy pocos
segundos. Eso permite que, con los frameworks y herramientas adecuadas se pueda
lanzar la batera de pruebas unitarias completa mientras se est editando el cdigo
(sea la prueba o el cdigo de produccin).
Para que una prueba se considere unitaria no basta con que est escrita en un
framework xUnit, debe cumplir los principios FIRST [60], que es un acrnimo para:
[693]
Fast Las pruebas unitarias deberan ser muy rpidas. Como se ha dicho, todas las
pruebas unitarias de la aplicacin (o al menos del mdulo) deberan ejecutarse
en menos de 23 segundos.
Independent Cada prueba debe poder ejecutarse por separado o en conjunto, y en
cualquier orden, sin que eso afecte al resultado.
Repeatable La prueba debera poder ejecutarse mltiples veces dando siempre el
mismo resultado. Por este motivo no es buena idea incorporar aleatoriedad a
los tests. Tambin implica que la prueba debe poder funcionar del mismo modo
en entornos distintos.
Self-validating La prueba debe ofrecer un resultado concreto: pasa o falla, sin que el
programador tenga que leer o interpretar un valor en pantalla o en un chero.
Timely El test unitario debera escribirse justo cuando se necesite, es decir, justo antes
de escribir el cdigo de produccin relacionado, ni antes ni despus.
En cuanto al resto de las pruebas: sistema, integracin y aceptacin; deberan ejecutarse al menos una vez al da. Existe toda una disciplina, llamada integracin continua que trata sobre la compilacin, integracin y prueba de todo el sistema de forma
totalmente automtica, incluyendo la instalacin de dependencias e incluso el empaquetado y despliegue. Esta operacin puede hacerse cada vez que un programador
aade nuevo cdigo al repositorio o bien una vez al da si la aplicacin es muy grande. El objetivo es disponer de informacin precisa y actualizada sobre el estado de la
aplicacin en su conjunto y sobre los requisitos que est cumpliendo.
24.5.
Se incluye el archivo de cabecera de gtest (lnea 1) donde estn denidas las macros que vamos a utilizar para denir las pruebas. La lnea 4 dene una prueba llamada
Zero mediante la macro TEST para el casa de prueba (TestCase) FactorialTest.
La lnea 5 especica una expectativa: el resultado de invocar factorial(0) debe
ser igual a 1.
Adems del chero con la prueba debemos escribir el cdigo de produccin, su
chero de cabecera y un Makefile. Al escribir la expectativa ya hemos decidido el
nombre de la funcin y la cantidad de parmetros (aunque no es tipo). Veamos estos
cheros:
4 Inspirado
C24
1
2
3
4
5
6
[694]
CC=$(CXX)
CXXFLAGS=-Igtest/include -Igtest -pthread
LDFLAGS=-pthread -lpthread
GTEST_TARBALL=gtest-1.7.0.zip
all: factorial-test
factorial-test: factorial-test.o factorial1.o gtest_main.a
gtest_main.a: gtest-all.o gtest_main.o
ar rv $@ $^
gtest %.o: gtest/src/gtest %.cc
g++ $(CXXFLAGS) -c $<
gtest: $(GTEST_TARBALL)
unp $<
ln -s $(GTEST_TARBALL:.zip=) gtest
clean:
$(RM) factorial-test *.o *~
$(RM) *.a
Esta primera prueba no la hemos visto fallar porque sin el chero de produccin
ni siquiera podramos haberla compilado.
Aadamos ahora un segundo caso de prueba al chero:
[695]
Listado 24.7: factorial-test.cc: Pruebas para factorial()
1 TEST(FactorialTest, Positive) {
2
EXPECT_EQ(1, factorial(1));
3
EXPECT_EQ(2, factorial(2));
4 }
1 FAILED TEST
El resultado nos indica la prueba que ha fallado (lnea 8), el valor obtenido por
la llamada (lnea 11) y el esperado (lnea 12).
A continuacin se debe modicar la funcin factorial() para que cumpla la
nueva expectativa. Despus escribir nuevas pruebas que nos permitan comprobar que
la funcin cumple con su cometido para unos cuantos casos representativos.
24.6.
Dobles de prueba
La mayora de los lenguajes de programacin que soportan la orientacin a objetos tienen herramientas para encapsulacin y ocultacin. La ocultacin (el hecho de
no exponer los detalles de implementacin de la clase) resulta crucial en un diseo
orientado a objetos porque proporciona sustituibilidad (LSP (Liskov Substitution
Principle)). Pero la ocultacin diculta la denicin de pruebas en base a aserciones
sobre de estado porque el estado del objeto est denido por el valor de sus atributos.
Si tuviramos acceso a todos los atributos del objeto (sea con getters o no) sera una
pista de un mal diseo.
C24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[696]
Debido a ello, suele ser ms factible denir la prueba haciendo aserciones sobre la
interaccin que el objeto que se est probando (el SUT (Subject Under Test)5 ) realiza
con sus colaboradores. El problema de usar un colaborador real es que ste tendr a
su vez otros colaboradores de modo que es probable que para probar un nico mtodo
necesitemos montar gran parte de la aplicacin. Como lo que queremos es instanciar
lo mnimo posible del sistema real podemos recurrir a los dobles6 de prueba.
Un doble de prueba es un objeto capaz de simular la interfaz que un determinado
colaborador ofrece al SUT, pero que realmente no implementa nada de lgica. El doble
(dependiendo de su tipo) tiene utilidades para comprobar qu mtodos y parmetros
us el SUT cuando invoc al doble.
Una regla bsica: Nunca se deben crear dobles para clases implementadas por
terceros, slo para clases de la aplicacin.
Un requisito importante para poder realizar pruebas con dobles es que las clases
de nuestra aplicacin permitan inyeccin de dependencias. Consiste en pasar (inyectar) las instancias de los colaboradores (dependencias) que el objeto necesitar en
el momento de su creacin. Pero no estamos hablando de un requisito impuesto por
las pruebas, se trata de otro de los principios SOLID, en concreto DIP (Dependency
Inversion Principle).
Aunque hay cierta confusin con la terminologa, hay bastante consenso en distinguir al menos entre los siguientes tipos de dobles:
Fake Es una versin rudimentaria del objeto de produccin. Funcionalmente equivalente, pero tomando atajos que no seran admisibles en el cdigo nal. Por
ejemplo, una base de datos cuya persistencia es un diccionario en memoria.
Stub Devuelve valores predenidos para los mtodos que el SUT va a invocar. Se
trata de un colaborador que le dice al SUT lo que necesita oir pero nada ms.
Mock El mock se programa con una serie de expectativas (invocaciones a sus mtodos) que debera cumplirse durante la ejecucin de la prueba. Si alguna de esas
llamadas no se produce, u ocurre en una forma diferente a lo esperado, la prueba
fallar.
Spy El spy es un objeto que registra todas las invocaciones que se hacen sobre l.
Despus de utilizado, se pueden hacer aserciones para comprobar que ciertas
llamadas a sus mtodos ocurrieron. A diferencia del mock, puede haber recibido
otras invocaciones adems de las que se compruebas y el comportamiento sigue
siendo vlido.
24.7.
En esta seccin veremos un ejemplo muy simple de uso con google-mock, el framework de dobles C++ que complementa a google-test. Vamos a implementar el mtodo notify() de la clase Observable (tambin llamada Subject) del patrn
observador.
5 SUT:
6 Son
SOLID
SOLID (SRP, OCP, LSP, ISP, DIP)7
es una serie de 5 principios esenciales para conseguir diseos orientados a objetos de calidad. Estos son:
SRP Single Responsibility
OCP Open Closed
LSP Liskov Substitution
DIP Dependency Inversion
ISP Interface Segregation
[697]
#ifndef _OBSERVABLE_H_
#define _OBSERVABLE_H_
#include <vector>
#include "observer.h"
class Observable {
std::vector<Observer*> observers;
public:
void attach(Observer* observer);
void detach(Observer* observer);
void notify(void);
};
#endif
#include <algorithm>
#include <functional>
#include "observable.h"
#include "observer.h"
void
Observable::attach(Observer* observer) {
observers.push_back(observer);
}
void
Observable::detach(Observer* observer) {
observers.erase(find(observers.begin(), observers.end(),
observer));
}
void
Observable::notify(void) {
observers[0]->update();
}
#ifndef _OBSERVER_H_
#define _OBSERVER_H_
class Observer {
public:
virtual void update(void) = 0;
virtual ~Observer() {}
};
#endif
C24
Para escribir un test que pruebe el mtodo notify() necesitamos un mock para
su colaborador (el observador). El siguiente listado muestra la interfaz que deben
implementar los observadores:
[698]
#ifndef MOCK_OBSERVER_H
#define MOCK_OBSERVER_H
#include <gmock/gmock.h>
#include "observer.h"
class MockObserver : public Observer {
public:
MOCK_METHOD0(update, void());
};
#endif
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "observable.h"
#include "mock-observer.h"
TEST(ObserverTest, UpdateObserver) {
MockObserver observer;
EXPECT_CALL(observer, update()).Times(1);
Observable observable;
observable.attach(&observer);
}
observable.notify();
GMOCK_DIR = gmock
GTEST_DIR = gmock/gtest
CC
= g++
CXXFLAGS = -I $(GTEST_DIR)/include -I $(GTEST_DIR) -I $(GMOCK_DIR)
/include -I $(GMOCK_DIR) -pthread
6 LDLIBS
= -pthread -lpthread
7
8 TARGET
= observable-tests
9
10 vpath %.cc $(GMOCK_SRC)/src
11
12 $(TARGET): observable-tests.o observable.o libgmock.a
13
[699]
gmock: gmock-1.7.0.zip
unp $<
ln -s gmock-1.7.0 gmock
gmock-1.7.0.zip:
wget https://googlemock.googlecode.com/files/gmock-1.7.0.zip
libgmock.a: gtest-all.o gmock-all.o gmock_main.o
ar rv $@ $^
gtest-all.o: $(GTEST_DIR)/src/gtest-all.cc
g++ $(CXXFLAGS) -c $<
gmock %.o: $(GMOCK_DIR)/src/gmock %.cc
g++ $(CXXFLAGS) -c $<
test: $(TARGET)
./$<
clean:
$(RM) $(TARGET) *.o *~ *.a
Ejecutemos el Makefile:
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ make test
g++ -I /usr/src/gmock
-c -o observable-tests.o observable-tests.cc
g++ -I /usr/src/gmock
-c -o observable.o observable.cc
g++ -I /usr/src/gmock
-c -o gmock_main.o /usr/src/gmock/src/
gmock_main.cc
g++ -I /usr/src/gmock
-c -o gmock-all.o /usr/src/gmock/src/gmock-all
.cc
g++ observable-tests.o observable.o gmock_main.o gmock-all.o lpthread -lgtest -o observable-tests
$ ./observable-tests
Running main() from gmock_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from ObserverTest
[ RUN
] ObserverTest.UpdateObserver
observable-tests.cc:11: Failure
Actual function call count doesnt match EXPECT_CALL(observer, update
())...
Expected: to be called once
Actual: never called - unsatisfied and active
[ FAILED ] ObserverTest.UpdateObserver (1 ms)
[----------] 1 test from ObserverTest (1 ms total)
[----------]
[==========]
[ PASSED ]
[ FAILED ]
[ FAILED ]
1 FAILED TEST
C24
1
2
3
4
[700]
4
5
6
7
8
9
10
11
12
13 [----------] Global test environment tear-down
14 [==========] 1 test from 1 test case ran. (0 ms total)
15 [ PASSED ] 1 test.
res:
La prueba pasa. Ahora comprobemos que funciona tambin para dos observado-
Y ejecutamos la prueba:
1
2
3
4
5
6
7
8
9
10
11
$ make test
Running main() from gmock_main.cc
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 3 tests from ObserverTest
[ RUN
] ObserverTest.UpdateObserver
[
OK ] ObserverTest.UpdateObserver (0 ms)
[ RUN
] ObserverTest.NeverUpdateObserver
[
OK ] ObserverTest.NeverUpdateObserver (0 ms)
[ RUN
] ObserverTest.TwoObserver
observable-tests.cc:30: Failure
24.8. Limitaciones
[701]
12 Actual function call count doesnt match EXPECT_CALL(observer2, update
13
14
15
16
17
18
19
20
21
22
23
24
())...
Expected: to be called once
Actual: never called - unsatisfied and active
[ FAILED ] ObserverTest.TwoObserver (0 ms)
[----------] 3 tests from ObserverTest (1 ms total)
[----------]
[==========]
[ PASSED ]
[ FAILED ]
[ FAILED ]
1 FAILED TEST
Todo correcto, aunque sera conveniente una prueba adicional para un mayor nmero de observadores registrados. Tambin podramos comprobar que los observadores des-registrados (detached) efectivamente no son invocados, etc.
Aunque los ejemplos son sencillos, es fcil ver la dinmica de TDD.
24.8.
Limitaciones
Hay ciertos aspectos importantes para la aplicacin en los que TDD, y el testing
en general, tienen una utilidad limitada (al menos hoy en da). Las pruebas permiten
comprobar fcilmente aspectos funcionales, pero es complejo comprobar requisitos
no funcionales.
C24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[702]
Kent Beck
Captulo
25
Empaquetado y distribucin
Francisco Moya Fernndez
David Villa Alises
703
[704]
25.1.
25.1.1.
[705]
C25
MS Windows Installer
[706]
Sin embargo, para mayor legibilidad en este primer ejemplo vamos a construir
el archivo desde cero. Volvamos al ejemplo ogre-hello. Una vez generados los
ejecutables en modo Release tenemos un subdirectorio Release que contiene todos
los ejecutables, en este caso OgreHello.exe. Por s solo no puede ejecutarse,
[707]
necesita las bibliotecas de OGRE, los archivos del subdirectorio media y los archivos
de conguracin resources.cfg y plugins.cfg que se incluyen con el cdigo
fuente. Es bastante habitual copiar las DLL al subdirectorio Release para poder
probar el ejemplo.
La estructura del archivo wxs es la siguiente:
1 <?xml version="1.0" encoding="utf-8"?>
2 <Wix xmlns=http://schemas.microsoft.com/wix/2006/wi>
3
<Product Name=OgreHello 1.0
4
Id=06d99eb2-5f61-4753-a6fb-ba90166119cf
5
UpgradeCode=36a85162-a254-44b7-af91-44416d1c1935
6
Language=3082 Codepage=1252
7
Version=1.0.0 Manufacturer=UCLM>
8
9
<!-- aqui va la descripcion de los componentes del producto -->
10
11
</Product>
12 </Wix>
La etiqueta Wix se usa para envolver toda la descripcin del instalador. Dentro
de ella debe haber un Product que describe el contenido del archivo msi. Todos los
productos tienen los atributos Id y UpgradeCode que contienen cada uno un GUID
(Globally Unique Identier). Un GUID (o UUID) es un nmero largo de 128 bits, que
puede generarse de manera que sea muy poco probable que haya otro igual en ningn
otro sitio. Se utilizan por tanto para identicar de manera unvoca. En este caso se
identica el producto y el cdigo de actualizacin.
Todas las versiones del producto tienen el mismo Id, mientras que para cambio
de major version se genera otro nuevo GUID para UpgradeCode (al pasar de
1.x a 2.x, de 2.x a 3.x, etc.). El UpgradeCode es utilizado por Windows Installer
para detectar cuando debe eliminar la versin vieja para reemplazarla por la nueva o
simplemente reemplazar determinados archivos. Para generar un GUID nuevo puede
utilizarse, por ejemplo, un generador de GUIDs en lnea, como http://www.
guidgenerator.com/.
Los cdigos numricos de Language y Codepage se corresponden a los valores indicados por microsoft en http://msdn.microsoft.com/en-us/library/
Aa369771.aspx. En nuestro caso el idioma elegido es el espaol de Espaa y la
pgina de cdigos es la correspondiente al alfabeto latino.
Dentro de un producto pueden declararse multitud de aspectos sobre el instalador.
Lo primero es denir el paquete MSI que se va a generar:
<Package Id=*
Description=Instalador de OgreHello 0.1
Manufacturer=UCLM
InstallerVersion=100
Languages=3082
Compressed=yes
SummaryCodepage=1252 />
Cada nuevo paquete generado tendr su propio GUID. Por este motivo WIX
permite simplicar la construccin con el cdigo especial * que indica que debe
ser auto-generado por el compilador. El atributo InstallerVersion indica la
versin mnima de Windows Installer requerida para poder instalar el paquete. Si no
se implementan aspectos avanzados, siempre ser 100, que corresponde a la versin
1.0.
C25
1
2
3
4
5
6
7
[708]
En este caso hemos creado un directorio OgreHello 1.0 dentro del directorio
estndar para los archivos de programa (normalmente C:\Program Files).
Dentro de este directorio hemos hecho un subdirectorio media que contendr los
archivos de medios (recursos).
Ahora podemos aadir componentes dentro de estos directorios. Cada componente
es un conjunto de archivos muy fuertemente relacionado, hasta el punto de que no
tiene sentido actualizar uno sin actualizar los dems. En general se tiende hacia
componentes lo ms pequeos posibles (un solo archivo), con objeto de que se puedan
hacer parches ms pequeos. Por ejemplo, el ejecutable principal es un componente,
pero por simplicidad aadiremos tambin los archivos de conguracin.
1 <DirectoryRef Id=INSTALLDIR FileSource=.>
2
<Component Id=MainExecutable Guid=1e71f142-c7cd-4525-980b-78
3
4
5
6
7
8
9
10
ebcafedeb1>
<File Name=OgreHello.exe KeyPath=yes>
<Shortcut Id=startmenuOgreHello Directory="ProgramMenuDir"
Name="OgreHello 1.0"
WorkingDirectory=INSTALLDIR Icon=OgreHello.exe
IconIndex=0 Advertise=yes />
</File>
<File Name=plugins.cfg/>
<File Name=resources.cfg/>
</Component>
</DirectoryRef>
Cada componente tiene tambin un GUID que lo identica. En este caso contiene
tres archivos, el ejecutable y los dos archivos de conguracin. Adems, para el
ejecutable creamos tambin un atajo en el men de inicio de Windows.
El atributo KeyPath de los archivos se pone a yes solamente para un archivo
dentro del componente. Este archivo ser utilizado por Windows Installer para
identicar si el componente est previamente instalado.
[709]
Para simplicar el resto del paquete vamos a meter todas las bibliotecas de OGRE
en un nico componente. En un caso real probablemente convendra dividirlo para
permitir parches ms pequeos en caso de que no afecten a todas las bibliotecas de
OGRE.
1 <DirectoryRef Id=INSTALLDIR FileSource=$(env.OGRE_HOME)\bin\
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
release>
<Component Id=OgreDLLs Guid=373f56f3-82c8-4c94-a0f6d9be98d8d4af>
<File Name=OgreMain.dll KeyPath=yes/>
<File Name=OgrePaging.dll/>
<File Name=OgreProperty.dll/>
<File Name=OgreRTShaderSystem.dll/>
<File Name=OgreTerrain.dll/>
<File Name=cg.dll/>
<File Name=OIS.dll/>
<File Name=Plugin_BSPSceneManager.dll/>
<File Name=Plugin_CgProgramManager.dll/>
<File Name=Plugin_OctreeSceneManager.dll/>
<File Name=Plugin_OctreeZone.dll/>
<File Name=Plugin_ParticleFX.dll/>
<File Name=Plugin_PCZSceneManager.dll/>
<File Name=RenderSystem_Direct3D9.dll/>
<File Name=RenderSystem_GL.dll/>
</Component>
</DirectoryRef>
d870c90c36>
3
<File Name=Sinbad.mesh KeyPath=yes/>
4
<File Name=Sinbad.material/>
5
<File Name=sinbad_body.tga/>
6
<File Name=sinbad_clothes.tga/>
7
<File Name=sinbad_sword.tga/>
8
</Component>
9 </DirectoryRef>
-01005e59853c">
<RemoveFolder Id=ProgramMenuDir On=uninstall />
<RegistryValue Root=HKCU Key=Software\[Manufacturer]\[
ProductName] Type=string Value= KeyPath=yes />
5
</Component>
6 </DirectoryRef>
3
4
C25
1 <DirectoryRef Id=ProgramMenuDir>
2
<Component Id="ProgramMenuDir" Guid="b16ffa2a-d978-4832-a5f2
[710]
25.1.2.
Hasta ahora los paquetes que hemos construido no tienen ningn tipo de interaccin con el usuario. WIX permite aadir fcilmente todo tipo de interaccin con el
usuario para personalizar la instalacin.
Para activar estas caractersticas es necesario ejecutar light.exe con la extensin WixUIExtension:
> light.exe -ext WixUIExtension ogre-hello.wixobj
Por ejemplo, para aadir un formulario que pregunta al usuario dnde se desea
instalar tan solo tenemos que cambiar la seccin Feature de este modo:
1 <UIRef Id="WixUI_Mondo" />
2 <UIRef Id="WixUI_ErrorProgressText" />
3 <Feature Id=Complete Level=1
4
Title=OgreHello 1.0
5
Description=Componentes de OgreHello 1.0.
6
Display=expand
7
ConfigurableDirectory=INSTALLDIR>
8
<ComponentRef Id=MainExecutable />
9
<ComponentRef Id=OgreDLLs />
10
<ComponentRef Id=MediaFiles />
11
<ComponentRef Id=ProgramMenuDir />
[711]
12 </Feature>
Tan solo hemos aadido dos referencias externas y algunos atributos, como
Title, Description, Display y ConfigurableDirectory. El resultado
es el que se muestra en la gura.
Otras caractersticas
C25
25.1.3.
[712]
25.2.
5 http://www.debian.org/doc/manuals/maint-guide/index.en.html
[713]
25.2.1.
Pidiendo un paquete
C25
[714]
25.2.2.
25.2.3.
Estructura bsica
En este punto podemos utilizar el programa dh_make que nos ayuda a generar
las plantillas de cheros necesarios para conctruir el paquete Debian, pero antes de
eso debemos crear dos variables de entorno para el nombre y direccin de correo del
mantenedor:
DEBEMAIL="juan.nadie@gmail.com"
DEBFULLNAME="Juan Nadie"
export DEBEMAIL DEBFULLNAME
Estas variables son tiles para otras operaciones relacionadas con las construccin
de paquetes, por lo que es buena idea aadirlas al chero $HOME/.bashrc para
futuras sesiones. Despus de eso, ejecuta dh_make:
ogre-hello-0.1$ dh_make -f ../ogre-hello-0.1.tar.gz
Type of package: single binary, indep binary, multiple binary, library, kernel
module, kernel patch?
[s/i/m/l/k/n] s
Maintainer name : Juan Nadie
Email-Address
: juan.nadie@gmail.com
Date
: Wed, 24 Apr 2013 12:59:40 +0200
Package Name
: ogre-hello
Version
: 0.1
License
: blank
Type of Package : Single
Hit <enter> to confirm:
Done. Please edit the files in the debian/ subdirectory now. You should also
check that the ogre-hello Makefiles install into $DESTDIR and not in / .
La ejecucin de dh_make ha creado un subdirectorio llamado debian con muchos cheros. Muchos de ellos son ejemplos (extensin .ex) para distintos nes: pgina de manual en distintos formatos, acciones durante la instalacin o desinstalacin
del paquete, script para creacin de un servicio en background (daemon), integracin
con cron, y muchos otros usos. Entre todos ellos hay cuatro cheros esenciales que
todo paquete Debian debe tener. A continuacin de muestran y describen, una vez
modicados segn corresponde con el paquete en cuestin. Comprelos con los generados por dh_make.
manpages
Todo chero ejecutable en el PATH
debera tener su pgina de manual. Las pginas de manual estn escritas en groff, pero es mucho ms sencillo crearlas a partir de formatos ms sencillos como
SGML (Standard Generalized Markup Language), XML, asciidoc o
reStructuredText y convertirlas en
el momento de la construccin del
paquete.
[715]
debian/control
El chero debian/control es una especie de maniesto del paquete. Incluye
informacin esencial para el sistema de gestin de paquetes. Veamos el chero
generado:
Listado 25.1: dh_make: debian/control
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Source: ogre-hello
Section: unknown
Priority: extra
Maintainer: Juan Nadie <Juan.Nadie@gmail.com>
Build-Depends: debhelper (>= 8.0.0)
Standards-Version: 3.9.4
Homepage: <insert the upstream URL, if relevant>
#Vcs-Git: git://git.debian.org/collab-maint/ogre-hello.git
#Vcs-Browser: http://git.debian.org/?p=collab-maint/ogre-hello.git;
a=summary
Package: ogre-hello
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: <insert up to 60 chars description>
<insert long description, indented with spaces>
Maintainer
Es el nombre y la direccin de correo electrnico de la persona que mantiene el
paquete actualmente.
Build-Depends
Es una lista de nombres de paquetes (opcionalmente con las versiones requeridas) que son necesarios para compilar el presente paquete. Estos paquetes deben
estar instalados en el sistema (adems del paquete build-essential) para
poder construir los paquetes binarios.
Standards-Version
La versin ms reciente de la policy que cumple el paquete.
C25
Priority
La importancia que tiene el paquete para la instalacin. Las categoras son:
required, important, standard, optional y extra. Este paquete, al ser un juego y
no causar ningn conicto, es optional. Puede ver el signicado de cada una
de estas prioridades en http://www.debian.org/doc/debian-policy/
ch-archive.html.
[716]
Package: ogre-hello
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, libogre-1.8.0
Description: minimal packaged Ogre example
minimal Ogre example to demostrate Debian packaging
[717]
debian/changelog
Contiene una descripcin breve de los cambios que el mantenedor hace a los
cheros especcos del paquete (los que estamos describiendo). En este chero no
se describen los cambios que sufre el programa en s; eso corresponde al autor del
programa y normalmente estarn en un chero CHANGES (o algo parecido) dentro del
.tgz que descargamos.
El siguiente listado muestra el chero changelog generado por dh_make:
Listado 25.3: debian/changelog para ogre-hello
1 ogre-hello (0.1-1) unstable; urgency=low
2
3
* Initial release (Closes: #nnnn) <nnnn is the bug number of your ITP>
4
5 -- Juan Nadie <Juan.Nadie@gmail.com> Wed, 24 Apr 2013 12:59:40 +0200
Cada vez que el mantenedor haga un cambio debe crear una nueva entrada como
esa al comienzo del chero. Fjese que la versin del paquete est formada por el
nmero de versin del programa original ms un guin y un nmero adicional que
indica la revisin del paquete debian. Si el mantenedor hace cambios sobre el paquete
manteniendo la misma versin del programa ir incrementando el nmero tras el
guin. Sin embargo, cuando el mantenedor empaqueta una nueva versin del programa
(supongamos la 0.2 en nuestro ejemplo) el nmero tras el guin vuelve a empezar
desde 1.
Como se puede apreciar, la primera versin del paquete debera cerrar (solucionar)
un bug existente. Ese bug (identicado por un nmero) corresponde al ITP que el
mantenedor debi enviar antes de comentar con el proceso de empaquetado.
Cuando una nueva versin del paquete se sube a los repositorios ociales, las
sentencias Closes son procesadas automticamente para cerrar los bugs noticados
a travs del DBTS. Puedes ver ms detalles en http://www.debian.org/doc/
debian-policy/ch-source.html#s-dpkgchangelog
debian/copyright
Este chero debe contender toda la informacin sobre el autor del programa y las
licencias utilizadas en cada una de sus partes (si es que son varias). Tambin debera
indicar quin es el autor y la licencia de los cheros del paquete debian. El chero
generado por dh_make contiene algo similar a:
Listado 25.4: dh_make: debian/copyright
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ogre-hello
Source: <url://example.com>
Files: *
Copyright: <years> <put authors name and email here>
<years> <likewise for another author>
License: <special license>
<Put the license of the package here indented by 1 space>
<This follows the format of Description: lines in control file>
.
<Including paragraphs>
# If you want to use GPL v2 or later for the /debian/* files use
# the following clauses, or change it to suit. Delete these two lines
Files: debian/*
Copyright: 2013 Juan Nadie <Juan.Nadie@gmail.com>
License: GPL-2+
This package is free software; you can redistribute it and/or modify
C25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[718]
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#
#
#
#
El chero debera contener una seccin (que comienza con Files:) por cada
autor y licencia involucrados en el programa. La seccin Files: debian/* ya
est completa y asume que el mantenedor va a utilizar la licencia GPL-2 y superior
para los cheros del paquete.
Debemos modicar este chero para incluir las licencias especcas del programa
que estamos empaquetando. Las secciones nuevas seran:
Listado 25.5: debian/copyright para ogre-hello
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
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ogre-hello
Source: http://www.ogre3d.org/tikiwiki/Sinbad+Model
Files: *
Copyright: 2013 ogre.developers@ogre.com
License: GPL-2
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
Files: media/*
Copyright: 2009-2010 Zi Ye <omniter@gmail.com>
License:
This work is licensed under the Creative Commons Attribution-Share
Alike 3.0 Unported License. To view a copy of this license, visit
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
Creative Commons, 171 Second Street, Suite 300, San Francisco,
California, 94105, USA.
.
This character is a gift to the OGRE community
(http://www.ogre3d.org). You do not need to give credit to the
artist, but it would be appreciated. =)
[719]
debian/rules
Se trata de un chero Makefile de GNU Make que debe incluir un serie
de objetivos: clean, binary, binary-arch, binary-indep y build. Estos objetivos son
ejecutados por el programa dpkg-buldpackage durante la construccin del
paquete.
El listado muestra el chero rules generado:
Listado 25.6: dh_make: debian/rules
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/make -f
# -*- makefile -*# Sample debian/rules that uses debhelper.
# This file was originally written by Joey Hess and Craig Small.
# As a special exception, when this file is copied by dh-make into a
# dh-make output file, you may use that output file without restriction.
# This special exception was added by Craig Small in version 0.37 of dh-make.
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
%:
dh $@ --with quilt
Por fortuna, actualmente el programa debhelper (o dh) hace la mayor parte del
trabajo aplicando reglas por defecto para todos los objetivos. Solo es necesario sobreescribir (reglas override_ dichos objetivos en el chero rules si se requiere un
comportamiento especial.
El listado anterior contiene una pequea diferencia respecto al generado por
dh_make que consiste en la adicin del parmetro -with quilt. El programa
quilt es una aplicacin especializada en la gestin de parches de un modo muy
cmodo.
25.2.4.
Para construir el paquete debe tener instalados todos los
paquetes listados en los campos
Build-Depends
y
Build-Depends-Indep,
adems
del
paquete
build-essential.
Una vez contamos con los cheros bsicos se puede proceder a una primera
compilacin del paquete. Para ello utilizamos el programa dpkg-buildpackage.
El siguiente listado muestra el comando y parte del resultado. Se omiten algunas partes
dado que la salida es bastante verbosa:
ogre-hello-0.1$ dpkg-buildpackage -us -us
dpkg-buildpackage: source package ogre-hello
dpkg-buildpackage: source version 0.1-1
dpkg-buildpackage: source changed by Juan Nadie <Juan.Nadie@gmail.com>
dpkg-buildpackage: host architecture amd64
dpkg-source --before-build ogre-hello-0.1
dpkg-source: info: applying make-install
fakeroot debian/rules clean
dh clean --with quilt
dh_testdir
dh_auto_clean
make[1]: Entering directory /home/david/repos/ogre-hello/ogre-hello-0.1
rm -f helloWorld *.log *.o *~
make[1]: Leaving directory /home/david/repos/ogre-hello/ogre-hello-0.1
dh_quilt_unpatch
dpkg-source -b ogre-hello-0.1
dpkg-source: info: using source format 3.0 (quilt)
dpkg-source: info: applying make-install
dpkg-source: info: building ogre-hello using existing ./ogre-hello_0.1.orig.tar.
gz
dpkg-source: info: building ogre-hello in ogre-hello_0.1-1.debian.tar.gz
dpkg-source: info: building ogre-hello in ogre-hello_0.1-1.dsc
debian/rules build
dh build --with quilt
[...]
dh_testdir
C25
build-depends
[720]
ogre-hello_0.1-1_amd64.changes
Este chero es utilizado por el archivo (el repositorio) de paquetes de Debian
y se utiliza en la subida (upload) de paquetes por parte de los DD (Debian
Developer). Contiene checksum para los otros cheros y, en una versin ocial,
debera estar rmado digitalmente para asegurar que el paquete no ha sido
manipulado por terceros.
ogre-hello_0.1-1.debian.tar.gz
Es un archivo que contiene todos los cambios que el mantenedor del paquete ha
hecho respecto al fuente del programa original, es decir, el directorio debian
principalmente.
ogre-hello_0.1-1_amd64.deb
. Es el paquete binario resultado de la compilacin (en concreto para la
arquitectura AMD-64) que puede ser instalado con dpkg o indirectamente con
los gestores de paquetes apt-get, aptitude u otros.
[721]
.
Aunque aparentemente todo ha ido bien y se ha generado el chero .deb vamos
a comprobar que hay algunos problemas graves por resolver. La forma ms ecaz de
ver estos problemas en utilizar el programa lintian:
ogre-hello-0.1$ lintian ../ogre-hello\_0.1-1\_amd64.changes
W: ogre-hello: empty-binary-package
Este aviso indica que el paquete no contiene nada aparte de los cheros aportados
por el propio sistema de construccin. Veamos qu contiene realmente:
ogre-hello-0.1$ debc ../ogre-hello_0.1-1_amd64.deb
new debian package, version 2.0.
size 1192818 bytes: control archive=760 bytes.
352 bytes,
10 lines
control
836 bytes,
11 lines
md5sums
Package: ogre-hello
Version: 0.1-1
Architecture: amd64
Maintainer: Juan Nadie <Juan.Nadie@gmail.com>
Installed-Size: 2814
Depends: libc6 (>= 2.2.5), libgcc1 (>= 1:4.1.1), libogre-1.8.0, libois-1.3.0,
libstdc++6 (>= 4.2.1)
Section: games
Priority: optional
Description: minimal packaged Ogre example
minimal Ogre example to demostrate Debian packaging
*** Contents:
drwxr-xr-x root/root
drwxr-xr-x root/root
drwxr-xr-x root/root
drwxr-xr-x root/root
drwxr-xr-x root/root
-rw-r--r-- root/root
copyright
-rw-r--r-- root/root
changelog.Debian.gz
0
0
0
0
0
2348
2013-04-29
2013-04-29
2013-04-29
2013-04-29
2013-04-29
2013-04-29
14:05
14:05
14:05
14:05
14:05
13:15
./
./usr/
./usr/share/
./usr/share/doc/
./usr/share/doc/ogre-hello/
./usr/share/doc/ogre-hello/
hello/media/
C25
25.2.5.
[722]
8
9
Se debe a que no est permitido modicar los cheros extrados del tarball del
autor del programa. Es necesario crear un parche. Un parche es un chero que contiene
los cambios que es preciso realizar en otro chero para lograr un resultado concreto,
indicando el nmero de lnea y otro contenido que ayuda a las herramientas a localizar
el lugar correcto. Por suerte, el propio error nos ofrece una forma muy sencilla que
crear este parche:
ogre-hello-0.1$ dpkg-source --commit
dpkg-source: info: local changes detected, the modified files are:
ogre-hello-0.1/makefile
Enter the desired patch name: make-install
[723]
Aunque ahora el paquete contiene los cheros deseados y los instalar en su ruta
correcta, pero an tiene algunos problemas ya que el programa no estaba pensado para
trabajar con esta estructura de cheros:
Los cheros de conguracin de ogre se buscan en directorio actual, pero
queremos buscarlos en /usr/share/games/ogre-hello.
Para solucionarlo, editamos el chero ExampleApplication.h (lnea 168)
y, a continuacin, asignamos el valor /usr/share/games/ogre-hello/ a la
variable mResourcePath. Despus se ejecuta dpkg-source -commit y
escribimos resource-path como nombre del parche.
Con esto ltimo tendremos un total de tres parches y el programa ser funcional
tras la instalacin. Quedan an dos problemas (no tan graves) por resolver segn
informa lintian. El primero (binary-without-manpage) indica que todo chero
ejecutable en el PATH debera tener una pgina de manual. El segundo (hardeningno-relro) indica que el programa debera estar compilado con determinadas opciones
que evitan problemas comunes.
C25
[724]
25.2.6.
Las cuatro ltimas lneas de la salida de uscan conrman que tenemos empaquetada la ltima versin y que adems tenemos en disco el tarball del autor.
Una vez que disponemos de la nueva versin del programa, debemos crear una
nueva entrada en el chero debian/changelog (que se puede automatizar en
parte con el programa dch). Para aplicar los cambios del nuevo tarball puedes
utilizar el programa uupdate aunque uscan puede encargarse tambin de esto.
A continuacin debe comprobar que la construccin de la nueva versin es correcta
poniendo especial atencin a la aplicacin de los parches sobre la nueva versin del
cdigo fuente.
6 Como
ejemplo
vea
http://qa.debian.org/developer.php?login=
pkg-games-devel%40lists.alioth.debian.ora
[725]
25.2.7.
25.3.
Junto con .deb, el otro sistema de paquete de amplio uso es RPM (RPM Package
Manager). Este formato fue creado por la distribucin Red Hat y hoy en da es
utilizado por muchas otras: Fedora, SUSE, CentOS, Yellow Dog, Oracle Linux, etc.
El formato .rpm guarda muchas similitudes con .rpm. Existen paquetes de fuentes (con extensin .srpm o src.rpm), paquetes binarios por cada arquitectura (por
ejemplo .i386.rpm) y tambin independientes de arquitectura (.noarch.rpm).
El procedimiento para la creacin de un paquete .rpm es en muchos sentidos ms
anrquico que el equivalente .deb e histricamente los gestores de paquetes manejas
peor las dependencias (en especial las circulares).
Sin embargo, es sencillo crear un paquete .rpm a partir del equivalente .deb
mediante el programa alien:
C25
Captulo
26
Representacin Avanzada
Sergio Prez Camacho
Jorge Lpez Gonzlez
Csar Mora Castro
26.1.
Fundamentos
26.1.1.
Billboards
727
[728]
a)
n
u'
b)
n'
c)
n
r
Figura 26.2: En a) se puede observar un Billboard con el vector up y el vector normal. En b) y c) se muestran
ejemplos de cmo calcular uno de los vectores dependiendo del otro en el caso de no ser perpendiculares.
Figura 26.3: Screenshot de billboard alineado con la pantalla. Obtenido del videojuego libre Air Rivals.
26.1. Fundamentos
[729]
Billboard alineado a la pantalla
Estos son los tipos ms simples de billboard. Su vector up u siempre coincide con
el de la cmara, mientras que el vector normal n se toma como el inverso del vector
normal de la cmara (hacia donde la cmara est mirando). Estos dos vectores son
siempre perpendiculares, por lo que no es necesario recalcular ninguno con el mtodo
anterior. La matriz de rotacin de estos billboard es la misma para todos.
Por lo tanto estos billboard siempre estarn alienados con la pantalla, aun cuando
la cmara realice giros sobre el eje Z (roll), como se puede apreciar en la Figura 26.3.
Esta tcnica tambin puede utilizarse para sprites circulares como partculas.
Billboard orientado en el espacio
En el caso de tratarse de un objeto fsico, en el que el vector up debe corresponder
con el vector up del mundo, el tipo anterior no es el apropiado. En este caso, el vector
up del billboard no es de la cmara, si no el del mundo virtual, mientras que el vector
normal sigue siento la inversa del vector hacia donde mira la cmara. En este caso el
vector jo es el normal, mientras que el que se recalcula es el vector up.
Sin embargo, utilizar esta misma matriz de rotacin para todos los billboard puede
dar lugar a imprecisiones. Segn se ha explicado, estos tipos de billboard se mostraran
como en el caso a) de la Figura 26.5. Al estar lo sucientemente cerca de la cmara
puede sufrir una distorsin debido a la perspectiva del punto de vista.
La solucin a esta distorsin son los billboard orientados al punto de vista. El
vector up seguira siendo el mismo (el vector up del mundo), mientras que el vector
normal es el que une el centro del billboard con la posicin de la cmara. De esta
forma, cada billboard tendra su propia matriz de rotacin, y dicha distorsin no
existira. En el caso b) de la Figura 26.5 se aprecia este ltimo tipo.
C26
Evidentemente, este tipo de billboard es menos eciente, ya que cada uno debe
calcular su propia matriz de rotacin. Para paliar este aumento de consumo de
tiempo de cmputo, podra implementarse dos niveles de billboard dependiendo de
la distancia de estos a la cmara. Si estn lo sucientemente lejos, la distorsin es
mnima y pueden utilizarse los primeros, mientras que a partir de cierta distancia se
aplicaran los billboard orientados al punto de vista.
[730]
a)
b)
Cmara
Cmara
Figura 26.5: Billboards con el mismo vector normal que la cmara en a), mientras que en b) son orientados
al punto de vista.
Billboard axial
El ltimo tipo de billboard son los axiales. Estos no miran directamente a la
cmara, simplemente giran alrededor de un eje jo denido, normalmente su vector
up. Este tcnica suele utilizarse para representar rboles lejanos, como el de la
Figura 26.1. En este caso el vector jo es el vector up, mientras que el recalculado
es el normal.
El mayor problema con este tipo de billboard es que si la cmara se sita justo
encima del billboard (en algn punto de su eje de rotacin), la ilusin desapareca al no
mostrarse el billboard. Una posible solucin es aadir otro billboard horizontal al eje
de rotacin. Otra sera utilizar el billboard cuando el modelo est lo sucientemente
lejos, y cambiar a geometra tridimensional cuando se acerque.
26.1.2.
Sistemas de partculas
[731]
26.2.
Billboards individuales
Tambin es posible controlar la
representacin individual de cada
Billboard dentro de un BillboardSet, pero la penalizacin en rendimiento es mucho mayor. En la mayora de los casos es ms eciente
crear distintos BillboardSets.
Uso de Billboards
C26
[732]
createBillboardSet("BillboardSet",3);
billboardSet->setMaterialName("Cloud");
billboardSet->setDefaultDimensions(10.,10.);
billboardSet->setSortingEnabled(true);
billboardSet->createBillboard(Ogre::Vector3(0,0,0));
billboardSet->createBillboard(Ogre::Vector3(50,0,50));
billboardSet->createBillboard(Ogre::Vector3(-50,0,-50));
Ogre::SceneNode* node1 = _sceneManager->createSceneNode("Node1");
node1->attachObject(billboardSet);
_sceneManager->getRootSceneNode()->addChild(node1);
De las lneas 7-9 se crean tantos Billboard como capacidad se di al BillboardSet
en el momento de su creacin. A cada uno de ellos se le indica la posicin
al
relativa
SceneNode al que pertenece el BillboardSet. Por ltimo, en las lneas 11-13 se crea
un nodo y se adjunta el BillboardSet a l.
Capacidad dinmica
Se puede indicar al BillboardSet
que aumente de forma dinmica su
capacidad. As, cada vez que se pida un Billboard y no quede ninguno, se aumentar la capacidad al
doble automticamente. Este mtodo es potencialmente peligroso, sobretodo si se utiliza en algn tipo de
bucle.
[733]
26.2.1.
Tipos de Billboard
Ogre, por defecto, al crear un Billboard lo crea utilizando el tipo point billboard.
Este tipo se puede cambiar a travs del mtodo BillboardSet::setBillboardType, y
recibe un argumento del tipo BillboardType. Los tres tipos bsicos existentes son:
Point Billboard: se indican con el valor BBT_POINT, y se corresponden con
los Billboards alineados en la pantalla. Se trata del tipo ms simple, y no es
necesario indicar ningn parmetro adicional.
Oriented Billboard: se indican con los valores:
BBT_ORIENTED_COMMON
BBT_ORIENTED_SELF
26.2.2.
Aplicando texturas
C26
[734]
En el siguiente cdigo se va a utilizar esta tcnica para que, cada uno de los
Billboard, muestren un trozo de la textura asociada. En 2-9 se crea el escenario y
un BillboardSet de tipo point
y un material asociado llamado ABC, como la de la
Figura 26.9. De las lneas 11-19 se instancian los 4 Billboard. Se ha declarado el
puntero a Billboard b para poder referenciar cada uno de los Billboard segn se crean
y poder indicar las coordenadas de las texturas asociadas. Esto se hace mediante el
mtodo Billboard::setTexcoordRect, al cual se le pasa un objeto del tipo FloatRec,
indicando las coordenadas de la esquina superior izquierda y de la esquina inferior
derecha.
Losvalores de esa esquina estn en el rango de 0 a 1. Para terminar, de las
lneas 21-23 se crea el nodo y se adjunta el BillboardSet.
(0,0)
(0.5,0)
A B
C D
(0,0.5) (0.5,0.5)
(0,1)
(0.5,1)
(1,0)
(1,0.5)
(1,1)
createBillboardSet("AbcBillboardSet",4);
abcBillboardSet->setBillboardType(Ogre::BBT_POINT);
abcBillboardSet->setMaterialName("ABC");
abcBillboardSet->setDefaultDimensions(7.,7);
Ogre::Billboard* b;
b = abcBillboardSet->createBillboard(Ogre::Vector3(-15,0,0));
b->setTexcoordRect(Ogre::FloatRect(0.,0.,0.5,0.5));
b = abcBillboardSet->createBillboard(Ogre::Vector3(-5,0,0));
b->setTexcoordRect(Ogre::FloatRect(0.5,0.,1.,0.5));
b = abcBillboardSet->createBillboard(Ogre::Vector3(5,0,0));
b->setTexcoordRect(Ogre::FloatRect(0.,0.5,0.5,1.));
b = abcBillboardSet->createBillboard(Ogre::Vector3(15,0,0));
b->setTexcoordRect(Ogre::FloatRect(0.5,0.5,1.,1.));
Ogre::SceneNode* abcNameNode =_sceneManager->getRootSceneNode()->
createChildSceneNode("AbcNameNode");
abcNameNode->setPosition(0,10,0);
abcNameNode->attachObject(abcBillboardSet);
26.3.
[735]
mismo sitio. Esto es interesante si se quiere dejar una estela, por ejemplo, de humo.
Si se desea que las partculas ya creadas se trasladen con el nodo al que pertenece
el sistema, se puede indicar que el posicionamiento se haga referente al sistema de
coordenadas local.
Los sistemas de partculas deben tener siempre una cantidad lmite de estas, o
quota. Una vez alcanzado esta cantidad, el sistema dejar de emitir hasta que
se eliminen algunas de las partculas antiguas. Las partculas tienen un lmite
de vida congurable para ser eliminadas. Por defecto, este valor de quota es
10, por lo que puede interesar al usuario indicar un valor mayor en la plantilla.
Ogre necesita calcular el espacio fsico que ocupa un sistema de partculas (su
BoundingBox) de forma regular. Esto es computacionalmente costoso, por lo que
por defecto, deja de recalcularlo pasados 10 segundos. Este comportamiento se
puede congurar mediante el mtodo ParticleSystem::setBoundsAutoUpdated(), el
cual recibe como parmetro los segundos que debe recalcular la BoundingBox. Si
se conoce de antemano el tamao aproximado del espacio que ocupa un sistema de
partculas, se puede indicar a Ogre que no realice este clculo, y se le indica el tamao
jo mediante el mtodo ParticleSystem::setBounds(). De esta forma se ahorra mucho
tiempo de procesamiento. Se puede alcanzar un compromiso indicando un tamao
inicial aproximado, y luego dejando a Ogre que lo recalcule durante poco tiempo
pasados algunos segundos desde la creacin del sistema.
A continuacin se describen los dos elementos bsicos que denen los sistemas de
partculas en Ogre: los emisores y los efectores.
26.3.1.
Emisores
Los emisores denen los objetos que literalmente emiten las partculas a la escena.
Los distintos emisores que proporciona Ogre son:
Puntos: point. Todas las partculas son emitidas desde un mismo punto.
Caja: box. Las partculas son emitidas desde cualquier punto dentro de un
volumen rectangular.
Cilindro: cylinder. Las partculas son emitidas desde un volumen cilndrico
denido.
Elipsoide: ellipsoid. Las partculas se emiten desde un volumen elipsoidal.
Supercie de elipsoide: hollow elipsoid. Las partculas se emiten desde la
supercie de un volumen elipsoidal.
Anillo: ring. Las partculas son emitidas desde los bordes de un anillo.
La velocidad, frecuencia y direccin de emisin de las partculas es completamente congurable. Estos emisores se posicionan de forma relativa al SceneNode al que
pertenecen.
Las partculas no son emitidas en una lnea recta. Se debe especicar un ngulo
mediante el parmetro angle para denir el cono de emisin. Un valor 0 indica que se
emiten en lnea recta, mientras que un valor de 180 signica que se emite en cualquier
direccin. Un valor de 90 implica que se emiten de forma aleatoria en el hemisferio
centrado en el vector de direccin.
C26
[736]
Otros parmetros que se pueden congurar son la frecuencia de emisin (partculas/segundo), la velocidad (puede ser una velocidad establecida o aleatoria para cada
partcula), el tiempo de vida o TTL (Time To Live) (denido o aleatorio), y el tiempo
de emisin del sistema. Ms adelante se mostrarn ejemplos de uso de estos parmetros.
26.3.2.
Efectores
Los efectores o affectors realizan cambios sobre los sistemas de partculas. Estos
cambios pueden ser en su direccin, tiempo de vida, color, etc. A continuacin se
explican cada uno de los efectores que ofrece Ogre.
LinearForce: aplica una fuerza a las partculas del sistema. Esta fuerza
se indica mediante un vector, cuya direccin equivale a la direccin de la
fuerza, y su mdulo equivale a la magnitud de la fuerza. La aplicacin de
una fuerza puede resultar en un incremento enorme de la velocidad, por lo
que se dispone de un parmetro, force_application para controlar esto. El
valor force_application average ajusta el valor de la fuerza para estabilizar
la velocidad de las partculas a la media entre la magnitud de la fuerza y la
velocidad actual de estas. Por el contrario, force_application add deja que la
velocidad aumente o se reduzca sin control.
ColourFader: modica el color de una partcula mientras sta exista. El
valor suministrado a este modicador signica cantidad de cambio de una
componente de color en funcin del tiempo. Por lo tanto, un valor de red -0.5
decrementar la componente del color rojo en 0.5 cada segundo.
ColourFader2: es similar a ColourFader, excepto que el modicador puede
cambiar de comportamiento pasada una determinada cantidad de tiempo. Por
ejemplo, el color de una partcula puede decrementarse suavemente hasta el
50 % de su valor, y luego caer en picado hasta 0.
ColourInterpolator: es similar a ColourFader2, slo que se pueden especicar
hasta 6 cambios de comportamiento distintos. Se puede ver como una generalizacin de los otros dos modicadores.
Scaler: este modicador cambia de forma proporcional el tamao de la partcula
en funcin del tiempo.
Rotator: rota la textura de la partcula por bien un ngulo aleatorio, o a una
velocidad aleatoria. Estos dos parmetros son denidos dentro de un rango (por
defecto 0).
ColourImage: este modicador cambia el color de una partcula, pero estos
valores se toman de un chero imagen (con extensin .png, .jpg, etc.). Los
valores de los pxeles se leen de arriba a abajo y de izquierda a derecha. Por
lo tanto, el valor de la esquina de arriba a la izquierda ser el color inicial, y el
de abajo a la derecha el nal.
ColourFader
Un valor de -0.5 no quiere decir que
se reduzca a la mitad cada segundo,
y por lo tanto, nunca alcanzar el
valor de 0. Signica de al valor
de la componente (perteneciente al
intervalo de 0 a 1) se le restar 0.5.
Por lo tanto a un valor de blanco
(1) se reducir a negro (0) en dos
segundos.
[737]
26.3.3.
Ps","ringOfFire");
3
4
5
6 }
Y an hay ms!
Existen multitud de parmetros para congurar los sistemas de partculas, emisores y modicadores en
Ogre. En la API ocial se detallan
todos y cada uno de estos parmetros.
A continuacin
se declaran tantos emisores y modicadores como se deseen. En
las lneas 8-19 se declara un emisor del tipo anillo. Los parmetros especicados para
este emisor son:
angle: dene el ngulo de apertura con el que las partculas salen disparadas.
C26
[738]
2001
2003
NVIDIA GeForce 3
NVIDIA GeForce FX
Hito
Provee un pixel framebuffer que la CPU debe encargarse de llenar.
Rasteriza y texturiza tringulos con vrtices pre-transformados.
Aplica tanto transformaciones, como iluminacin a los vrtices. Usa una xed
function pipeline.
Incluye pixel shader congurable y vertex shader completamente programable.
Primera tarjeta con shaders completamente programables.
Cuadro 26.1: Evolucin del hardware de grco para PC.
26.4.
26.4.1.
Un poco de historia
Nuestra narraccin comienza en los aos 80, una poca que hoy en da a algunos
les puede parecer como un tiempo mejor, pero que denitivamente no lo era, sobre
todo si queras dedicarte al desarrollo de aplicaciones grcas interactivas.
En estos aos el desarrollo de este tipo de aplicacines era realmente complicado
y no slo por la poca capacidad de las mquinas de la poca, si no tambin por la
falta de estandarizacin que exista en los APIs grcos, lo que provocaba que cada
hardware necesitara de su propio software para poder ser usado.
Este contexto haca evidente que los grcos 3D tardaran todava un poco en
llegar a ser algo comn en los ordenadores personales y aun ms si hablamos de
moverlos en tiempo real. Es por tanto fcil comprender por qu los mayores avances
en este campo estuvieron enfocados sobre todo hacia el renderizado no interactivo.
En aquellos tiempos la principal utilidad que tena la informtica grca eran la
investigacin y el desarrollo de CGI (Computer Generated Imagery) para anuncios
y pelculas.
Es en este punto donde la historia de los shaders comienza, en una pequea
empresa conocida como LucasFilms a principios de los aos 80. Por esas fechas el
estudio decidi contratar programadores grcos para que, comandados por Edwin
Catmull, empezaran a informatizar la industria de los efectos especiales. Casi nada.
Aquel grupo de pioneros se embarc en varios proyectos diferentes, logrando que
cada uno de ellos desembocara en cosas muy interesantes. Uno de estos proyectillos
acab siendo el germen de lo que ms adelante se conocera como Pixar Animation
Studios. Y fue en esta compaa donde, durante el desarrollo del API abierto RISpec
C26
Ao
1987
1996
1999
[739]
[740]
Pipeline
En ingeniera del software, un pipeline consiste en una cadena etapas
de procesamiento, dispuestas de tal
forma que la salida de una de estas
etapas es la entrada de la siguiente,
facilitando con ello la ejecucin en
paralelo de las etapas.
[741]
La evolucin de la tecnologa para construir los hardware grcos y su correspondiente incremento en la capacidad de procesamiento acab permitiendo trasladar la
idea de RenderMan a los ordenadores domsticos en el ao 2001, el cual nos trae a
dos protagonistas de excepcin: la NVIDIA GeForce3 que introdujo por primera vez
un pipeline grco programable (aunque limitado) y DirectX 8.0 que nos di el API
para aprovechar esas capacidades.
Desde entonces, como suele pasar en la informtica, con el paso de los aos todo
fue a mejor, el hardware y los API grcos no han hecho sino dar enormes saltos hacia
delante tanto en funcionalidad como en rendimiento (un ejemplo de la evolucin de las
capacidades de las tarjetas grcas se presenta en la tabla 26.1). Lo cual nos ha llevado
al momento actual, en el que los FFP han sido casi sustituidos por los programmablefunction pipelines y sus shader para controlar el procesamiento de los grcos.
Hasta aqu el repaso a la historia de los shaders, que nos ha permitido introducir
algunos conceptos bsicos que siempre os acompaaran si os animis a seguir por este
campo: pipeline, programmable-function pipeline, xed-function pipeline. Pero...
26.4.2.
Y qu es un Shader?
Una de las deniciones clsicas de shader los muestra como: piezas de cdigo,
que implementan un algoritmo para establecer como responde un punto de una
supercie a la iluminacin. Es decir sirven para establecer el color denitivo de
los pixeles que se mostrarn en pantalla.
En algn momento comprenderis
que hacer videojuegos, al nal, no
es ms que gestionar recursos y balancear calidad de imagen con rapidez de ejecucin. Por eso conocer el
funcionamiento de un motor grco, conocimientos altos de ingeniera del software y gestin de recursos en un programa software son de
las habilidades ms demandadas.
Como veremos esta denicin ya no es del todo correcta. Los shader actuales
cumplen muchas ms funciones, ya que pueden encargarse de modicar todo tipo
de parmetros (posicin de vrtices, color de pixeles e incluso generar geometra en
los ms avanzados). Hoy por hoy una denicin de shader ms acertada podra ser:
Un conjunto de software que especica la forma en que se renderiza un conjunto
de geometra, denicin extraida del libro GPU Gems [33], ideal para aprender
sobre tcnicas avanzadas de programacin grca.
Desde un punto de vista de alto nivel los shader nos permiten tratar el estado de
renderizado como un recurso, lo cual los convierte en una herramienta extremadamente poderosa, permitiendo que el dibujado de una aplicacin, es decir, la conguracin
del dispositivo que se encarga de ello sea casi completamente dirigido por recursos
(de la misma forma en que la geometra y las texturas son recursos externos a la aplicacin). Facilitando de esta manera su reusabilidad en mltiples proyectos.
Las deniciones, en mi opinin, suelen ser siempre algo esotricas para los
profanos en una materia, por ello la mejor manera de terminar de explicar qu son
y cmo funcionan estas pequeas piezas de cdigo es repasando el funcionamiento
del pipeline grco: desde la transmisin de los vrtices a la GPU, hasta la salida por
pantalla de la imagen generada.
26.4.3.
Pipelines Grcos
C26
Gestion-Recursos
[742]
26.4.4.
Fixed-Function Pipeline
Primitiva
Una primitiva es una regin de una
supercie denida por sus vrtices.
La primitiva ms usada en informtica grca es el tringulo, puesto
que sus tres vrtices siempre denen un plano.
[743]
Conectividad de vrtices
Fragmentos
Coloreados
Fragmentos
Ensamblado
de Primitivas
y Rasterizacin
Transformacin
del Vrtice
Vrtices
Interpolacin,
Texturizado
y Coloreado
Operaciones
de Rasterizacin
Pxeles
Actualizados
Vrtices
Transformados
Transformacin de Vrtices
El conjunto de vrtices primero atraviesa esta etapa de procesamiento del pipeline
grco, en la que se realizan una serie de operaciones matemticas sobre los mismos.
Estas operaciones incluyen las transformaciones necesarias para convertir la posicin
del vrtice a la posicin que tendr en pantalla (Transformacin de Modelado,
Transformacin de Visualizacin y Transformacin de Proyeccin visto en seccin
1.1.2 del Mdulo 2) y que ser usada por el rasterizador, la generacin de las
coordenadas de textura y el clculo de la iluminacin sobre el vrtice para conocer
su color.
Ensamblado de Primitivas y Rasterizacin
Los vrtices transformados uyen en secuencia hacia la siguiente etapa donde
el ensamblado de primitivas toma los vrtices y los une para formar las primitivas
correspondientes gracias a la informacin recibida sobre la conectividad de los
mismos (que indica cmo se ensamblan).
C26
Textura
[744]
Scissors Test
Este test permite restringir el rea
de dibujado de la pantalla, descartando as, todos aquellos fragmentos que no entren dentro.
Alpha Test
Permite descartar fragmentos comparando el valor de alpha de cada
fragmento, con un valor constante
especicado.
Stencil Test
A partir del uso del stencil buffer,
hace una comparacin con el framebuffer, y descarta aquellos fragmentos que no superen la condicin especicada. Como si usara una mscara, o una plantilla, para especicar qu se dibuja y qu no.
[745]
La fase en que se ejecutan las operaciones de rasterizacin pone en marcha una
secuencia de tests para cada cada fragmento, inmediatamente antes de actualizar
el framebuffer. Estas operaciones son una parte estndar de OpenGL y Direct3D,
e incluyen: el scissor test, alpha test, stencil test y el depth test. En ellos estn
involucrados el color nal del fragmento, su profundidad, su localizacin, as como su
valor de stencil.
Si cualquier test falla, esta etapa descarta el fragmento sin actualizar el valor de
color del pixel (sin embargo, podra ocurrir una operacin de escritura para el valor
stencil). Pasar el depth test puede reemplazar el valor de profundidad del pixel, por
el valor de profundidad del fragmento. Despues de los tests, la operacin de blending
combina el color nal del fragmento con el valor de color del pixel correspondiente.
Finlmente, con una operacin de escritura sobre el framebuffer se reemplaza el color
del pixel, por el color mezclado.
Conclusin
26.4.5.
Programmable-Function Pipeline
C26
Depth Test
[746]
Aplicacin 3D
Comandos del API 3D
API 3D:
OpenGL o
Direct3D
Lmite CPU - GPU
Comandos
de la GPU
y flujo de datos.
Primitivas
ensambladas
Flujo de ndices
de vrtices
GPU
Front End
Ensamblado
de Primitivas
Vrtices
pre-transformados
Vrtices
transformados
Primitivas
ensambladas
Procesador
Programable
de Geometra
Flujo de localizaciones
de pxeles
Rasterizacin
e Interpolacin
Operaciones
de Raster
Fragmentos
pre-transformados
Procesador
Programable
de Vrtices
Actualizaciones
de pxeles
Fragmentos
transformados
Procesador
Programable
de Fragmentos
Cada vertex shader afecta a un slo vrtice, es decir, se opera sobre vrtices
individuales no sobre colecciones de ellos. A su vez, tampoco se tiene acceso
a la informacin sobre otros vrtices, ni siquiera a los que forman parte de su
propia primitiva.
Framebuffer
[747]
Al igual que los vertex shader, slo puede procesar un fragmento cada vez y
no puede inuir sobre, ni tener acceso a, ningn otro fragmento.
1
2
3
4
5
6
7
8
9
C26
[748]
10
11
12
13
14
15 }
26.4.6.
[749]
Vertex Displacement Mapping
Los vrtices pueden ser desplazados, por ejemplo verticalmente, en funcin de los
resultados ofrecidos por un algoritmo o mediante la aplicacin de un mapa de alturas
(una textura en escala de grises), consiguiendo con ello, por ejemplo, la generacin de
un terreno irregular.
Screen Effects
Los shader pueden ser muy tiles para lograr todo tipo de efectos sobre la imagen
ya generada tras el renderizado. Gracias a las mltiples pasadas que se pueden aplicar,
es posible generar todo tipo de efectos de post-procesado, del estilo de los que se usan
en las pelculas actuales.
Los fragment shader realizan el renderizado en una textura temporal (un framebuffer alternativo) que luego es procesada con ltros antes de devolver los valores de
color.
C26
[750]
una altsima calidad, aunque habra que tener cuidad con su uso. El conseguir, por
ejemplo, luz por pixel implicara unas operaciones bastante complejas por cada pixel
que se vaya a dibujar en pantalla, lo cual, contando los pixeles de una pantalla, pueden
ser muchas operaciones.
Figura 26.17: Diferencia entre Per-Vertex Lighting (izquierda) y Per-Pixel Lighting (derecha).
Non-Photorealistic Rendering
Los modelos de iluminacin no tienen porque limitarse a imitar el mundo real,
pueden asignar valores de color correspondientes a mundos imaginarios, como puede
ser el de los dibujos animados o el de las pinturas al oleo.
[751]
26.4.7.
Lenguajes de Shader
Existe una gran variedad de lenguajes para escribir shaders en tiempo real y no
parece que vayan a dejar de aparecer ms. Estos pueden agruparse en dos categoras:
los basados en programas individuales o los que se basan en cheros de efectos.
La primera aproximacin necesita que se cree una coleccin de cheros, cada uno
de los cuales implementa un Vertex Shader o un Fragment Shader en una pasada de
renderizado. Ejemplos de estos lenguajes son: Cg, HLSL o GLSL. Lo cual conlleva
una cierta complejidad en su gestin pues para cada pasada de renderizado, en los
efectos complejos, necesita de dos cheros diferentes.
Para solucionar este problema surgen el siguiente tipo de lenguajes que son
aquellos basados cheros de efectos, de los cuales, el ms conocido es el CgFX de
NVidia, y que a su vez es un super conjunto del Microsoft effect framework.
Los lenguajes basados en cheros de efectos permiten que los diferentes shader
se incluyan en un mismo chero y, adems, introducen dos nuevos conceptos: tcnica
y pasada. En los cuales pueden ser agrupados los diferentes shaders y permiten que el
estado del dispositivo grco pueda ser denido en cada pasada de renderizado.
Con esta aproximacin se consigue que la gestin sea ms manejable, mejor
dirigida por recursos y mucho ms poderosa.
26.5.
C26
http://developer.nvidia.com/object/cg_tutorial_home.html
[752]
Por otro lado, es conveniente tener a mano el chero de log que genera Ogre en
cada ejecucin, para saber por qu nuestro shader hace que todo aparezca blanco (o
lo que es lo mismo por qu ha fallado su compilacin?).
26.5.1.
Primer Shader
Este primer shader servir de toma de contacto para familiarizarnos con algunos
de los conceptos que introduce. Es por eso que no hace prcticamente nada, slo deja
que el vrtice pase a travs de el, modicando nicamente su color y envindolo a la
siguiente etapa.
La escena 3D puede ser cualquiera, siempre y cuando alguna de las entidades
tenga asociado el material que se denir ahora.
Lo primero es indicarle a Ogre alguna informacin sobre los shader que queremos
que use, la cual debe estar incluida en el propio archivo de material.
Al denir el shader que usar nuestro material hay que indicar al menos:
El nombre del shader
En qu lenguaje est escrito
En qu chero de cdigo est almacenado
Cmo se llama el punto de entrada al shader
En qu perl queremos que se compile
Por ltimo, antes de denir el material en si, hay que indicar tambin a los shader
cuales son aquellos parmetros que Ogre les pasar. En este caso slo pasamos la
matriz que usaremos para transformar las coordenadas de cada vrtice a coordenadas
de cmara. Es importante denir esta informacin al principio del chero de material.
Log de Ogre
En el chero de log podremos encontrar informacin sobre los perles y funcionalidad soportada por
nuestra tarjeta grca, as cmo informacin sobre posibles errores en
la compilacin de nuestros materiales o shaders.
[753]
Listado 26.7: Declaracin del vertex shader en el material
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Parmetros usados
default_params
{
param_named_auto worldViewMatrix worldviewproj_matrix
}
Para este caso, el material quedara tal y como se ve en el listado 26.8. Y queda
claro cmo en la pasada se aplican los dos shader.
Listado 26.8: Primer Material con shaders
Declaracin shaders
Para ms informacin sobre la
declaracin de los shader, sera buena idea dirigirse al manual en: http://www.ogre3d.org/docs/manual/manual_18.html
vertex_program VertexGreenColor cg
{
source firstshaders.cg
entry_point v_green_color
profiles vs_1_1 arbvp1
default_params
{
param_named_auto worldViewMatrix worldviewproj_matrix
}
fragment_program FragmentColorPassthrough cg
{
source firstshaders.cg
entry_point f_color_passthrough
profiles ps_1_1 arbfp1
}
material VertexColorMaterial
{
technique
{
pass
{
vertex_program_ref VertexGreenColor
{
}
fragment_program_ref FragmentColorPassthrough
{
}
Los dos programas que denen nuestros primeros fragment y vertex shader
aparecen en los listados 26.9 y 26.10, y los dos deberan incluirse en el chero
rstshaders.cg (o en el archivo que queramos), tal y como indicamos en el material.
C26
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
[754]
El vertex shader recibe cmo nico parmetro del vrtice su posicin, esta
es transformada a coordenadas del espacio de cmara gracias a la operacin que
realizamos con la variable que representa la matriz de transformacin.
Por ltimo se asigna el color primario al vrtice, que se corresponde con su
componente difusa, y que en este caso es verde.
Listado 26.10: Primer fragment shader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return OUT;
[755]
En los lenguajes de programacin clsicos, los tipos de datos representan magnitudes escalares (int, o float son ejemplos de ello). Pero, cmo hemos visto en la
seccin 26.4.5, las GPU estn preparadas para tratar con vectores (entiendolos como
una coleccin de magnitudes escalares). Los vectores en C o C++ pueden representarse fcilmente mediante arrays de valores escalares, sin embargo, como su procesamiento es fundamental en el tratamiento de vrtices o fragmentos, Cg ofrece tipos
predenidos para estas estructuras.
Para el tratamiento de vectores, podemos usar los siguientes tipos:
float2 uv_coord;
float3 position;
float4 rgba_color;
Estos tipos de datos son mucho ms ecientes que el uso de un array, por lo
que no es recomendable sustituir un float4 X, por un float X[4], ya que no son
exactamente lo mismo. Cg se encarga de almacenar esta informacin en una forma,
mediante la cual, es posible sacar mucho mejor partido de las caractersticas de una
GPU a la hora de operar con estos elementos.
Por otro lado, Cg soporta nativamente tipos para representar matrices. Ejemplos
de declaraciones seran:
float4x4 matrix1;
float2x4 matrix2;
Al igual que los vectores, el uso de estos tipos garantiza una forma muy eciente
de operar con ellos.
Otra de las cosas que seguramente pueden llamar la atencin, es la peculiar forma
en que se declaran los tipos de los atributos pertenecientes a la estructura usada
tVOutput, o tFOutput.
float4 position : POSITION;
A los dos puntos, y una palabra reservada tras la declaracin de una variable (como
POSITION o COLOR), es lo que, en Cg, se conoce como semntica. Esto sirve para
indicar al pipeline grco, qu tipo de datos deben llenar estas variables.
Para ms informacin sobre ellos se
recomienda consultar los captulos
2 y 3 del muy recomendable The
Cg Tutorial [34]
Es decir, como ya sabemos, los shader son programas que se ejecutan en ciertas
etapas del pipeline. Por lo tanto, estas etapas reciben cierta informacin que nosotros
podemos usar indicando con la semntica cules son estos datos. Los nicos sitios
donde se pueden usar son en estructuras de entrada o salida (como las de los ejemplos)
o en la denicin de los parmetros que recibe el punto de entrada (mtodo principal)
de nuestro shader.
Por ltimo, es necesario hablar brevemente sobre la palabra reservada uniform, su
signicado, y su conexin con lo denido en el chero de material.
El pipeline debe proveer de algn mecanismo para comunicar a los shader los
valores de aquellos elementos necesarios para conocer el estado de la simulacin (la
posicin de las luces, la delta de tiempo, las matrices de transformacin, etc...). Esto
se consigue mediante el uso de las variable declaradas como uniform. Para Cg, estas
son variables externas, cuyos valores deben ser especicados por otro elemento.
C26
Atributos especiales Cg
[756]
26.5.2.
createChildSceneNode("QuadNode");
12
13
quadNode->attachObject(ent);
14 }
15
16 Ogre::ManualObject* CPlaneExample::createManualPlane() {
17
// Creamos un cuadrado de forma manual
18
Ogre::ManualObject* manual = mSceneMgr->createManualObject("
Quad");
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
[757]
46
47 }
return manual;
El vertex shader lo nico que tendr que hacer es dejar pasar el vrtice, pero esta
vez, en vez de cambiarle el color dejamos que siga con el que se le ha asignado. Para
ello, en el mtodo que dene el punto de entrada del shader hay que indicarle que
recibe como parametro el color del vrtice.
Listado 26.12: Segundo vertex shader
Figura 26.21: El cuadrado aparece
con el color de sus cuatro vrtices
interpolado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
El fragment shader podemos dejarlo igual, no es necesario que haga nada. Incluso
es posible no incluirlo en el chero de material (cosa que no se puede hacer con los
vertex shader).
El resultado obtenido, debera ser el mostrado en la gura 26.21.
Ahora que sabes cmo modicar el color de los vrtices. Seras capaz
de pintar la entidad tpica del Ogro como si de textura tuviera un mapa
de normales? Figura 26.22 Tip: Has de usar la normal del vrtice para
conseguirlo.
26.5.3.
C26
Pintar un modelo con el color de sus vrtices puede ser interesante, pero desde
luego no es muy impresionante. Por lo tanto con este tercer shader usaremos una
textura para darle algo de detalle a nuestro plano anterior. Para ello necesitamos
acceder a las coordenadas de textura de nuestra entidad cuadrada, por lo que se las
aadiremos en nuestro mtodo createManualPlane.
[758]
// Vertex shader
struct tVOutput {
float4 position:
POSITION;
float2 uv :
TEXCOORD0;
};
// Usamos la posicin y la coordenada de textura
tVOutput v_uv_passthrough(
float4
position
:
POSITION,
float2
uv
:
TEXCOORD0,
uniform float4x4
worldViewMatrix)
{
tVOutput
OUT;
// Transformacin del vrtice
OUT.position = mul(worldViewMatrix, position);
OUT.uv = uv;
}
return OUT;
[759]
Listado 26.16: Segundo fragment shader. Mapeamos la textura en los fragmentos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct tFOutput {
float4 color
};
COLOR;
26.5.4.
Primero, podramos intentar devolver el color inverso de la textura que use nuestro
modelo, dando un efecto como en el negativo de las fotos.
Esto es muy sencillo de conseguir, slamente debemos restarle a 1 los valores
correspondiente del vector RGBA. Veamos cmo:
1
2
3
4
5
6
7
8
9
10
C26
[760]
11
12
13
14 }
2 tFOutput f_tex_red(
3
float2
uv : TEXCOORD0,
4
uniform sampler2D
texture)
5 {
6
tFOutput OUT;
7
OUT.color = tex2D(texture, uv);
8
OUT.color.r *= 0.5f;
9
OUT.color.bg *= 0.15f;
10
return OUT;
11 }
Por otro lado, entramos de lleno en el asunto de los perles. Los perles son una
forma de gestionar la funcionalidad de la que disponen los shader que programamos,
y de esta manera, saber en qu dispositivos podrn funcionar y qu funcionalidad
tenemos disponible.
Todos los ejemplos usados hasta ahora usaban los perles vs_1_1 y ps_1_1 para
DirectX 8, y arbvp1 y arbfp1 para OpenGL, que son los perles ms simples a la
vez que los ms ampliamente soportados. Sin embargo para disponer del swizzling es
necesario usar los ms avanzados vs_2_0 y ps_2_0, que son compatibles con DirectX
y OpenGL.
La declaracin del fragment shader en el material quedar, entonces, como se
muetra en el Listado 26.19.
Listado 26.19: Declaracin del fragment shader
1 fragment_program FragmentRedTex cg
2 {
3
source texshaders.cg
4
entry_point f_tex_red
5
profiles ps_2_0 arbfp1
[761]
6 }
trigonometricas
2 tFOutput f_tex_interference(
3
float2
uv : TEXCOORD0,
4
uniform sampler2D
texture)
5 {
6
tFOutput OUT;
7
OUT.color = tex2D(texture, uv);
8
OUT.color.r *= sin(uv.y*100);
9
OUT.color.g *= cos(uv.y*200);
10
OUT.color.b *= sin(uv.y*300);
11
return OUT;
12 }
1
2
3
4
5
6
7
8
9
10
Listado 26.22: La composicin del color de la textura en varias posiciones diferentes da lugar
a un efecto borroso
// Como si dibujaramos tres veces la textura
tFOutput f_tex_blurry(
float2
uv : TEXCOORD0,
uniform sampler2D
texture)
{
tFOutput OUT;
OUT.color = tex2D(texture, uv);
OUT.color.a = 1.0f;
OUT.color += tex2D(texture, uv.xy + 0.01f);
OUT.color += tex2D(texture, uv.xy - 0.01f);
return OUT;
}
C26
1
2
3
4
5
6
7
8
9
10
11
12
Listado 26.23: Este efecto se consigue con una combinacin del efecto blur con la conversin
del color a escala de grises
Figura
(blur).
26.29:
Efecto
borroso
[762]
5 {
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 }
26.5.5.
Antes nos hemos dedicado a jugar un poco con los fragment shader, por lo que
ahora sera conveniente hacer lo mismo con los vertex shader.
Primero usaremos el conocimiento hasta ahora recogido para, a partir de un slo
modelo, dibujarlo dos veces en posiciones distintas. Para conseguirlo se necesitar un
material que dena dos pasadas. Una primera en la que el personaje se dibujar en su
lugar, y otra en la que ser desplazado.
Listado 26.24: Denicin de las dos pasadas
1 material CopyObjectMaterial
2 {
3
technique
4
{
5
pass
6
{
7
vertex_program_ref VertexColorPassthrough
8
{
9
}
10
11
fragment_program_ref FragmentTexPassthrough
12
{
13
}
14
15
texture_unit
16
{
17
texture terr_rock6.jpg
18
}
19
}
20
21
pass
22
{
23
vertex_program_ref VertexDisplacement
24
{
25
}
26
27
fragment_program_ref FragmentTexPassthrough
28
{
29
}
30
31
texture_unit
32
{
33
texture terr_rock6.jpg
[763]
34
35
36
37 }
El anterior era un shader muy sencillo, por lo que ahora intentaremos crear
nuestras propias animaciones mediante shaders.
Para ello necesitamos tener acceso a la delta de tiempo. En la pgina ocial1
aparecen listados los diferentes parmetros que Ogre expone para ser accedidos
mediante variables uniform. En este caso, en la denicin del vertex shader realizada
en el material debemos indicar que queremos tener acceso a la delta de tiempo, como
se ve en el Listado 26.26.
Figura 26.31: Resultado de las dos
pasadas.
1 // A partir de la delta
2 // escalar el modelo en
3 tVOutput v_pulse(
4
float4
5
float2
6
uniform float
7
uniform float4x4
1 http://www.ogre3d.org/docs/manual/manual_23.html#SEC128
C26
Una vez tenemos accesible el tiempo transcurrido entre dos vueltas del bucle
principal, slo necesitamos pasarlo como parmetro a nuestro shader y usarlo para
nuestros propsitos. En este caso, modicaremos el valor del eje Y de todos los
vrtices del modelo, siguiendo una funcin cosenoidal, como se puede ver en el
Listado 26.32.
[764]
8 {
9
10
11
12
13
14
15
16 }
tVOutput
OUT;
Sabiendo cmo hacer esto, se tiene la posibilidad de conseguir muchos otros tipos
de efectos, por ejemplo en el siguiente se desplaza cada cara del modelo en la direccin
de sus normales.
Listado 26.28: Uso de la normal y la delta de tiempo para crear un efecto cclico sobre los
vrtices
1 tVOutput v_extrussion(
2
float4
position
:
POSITION,
3
float4
normal
:
NORMAL,
4
float2
uv
:
TEXCOORD0,
5
uniform float
pulseTime,
6
uniform float4x4
worldViewMatrix)
7 {
8
tVOutput
OUT;
9
OUT.position = position + (normal * (cos(pulseTime)*0.5f));
10
OUT.position = mul(worldViewMatrix, OUT.position);
11
OUT.uv = uv;
12
13
return OUT;
14 }
Seras capaz de montar una escena con un plano y crear un shader que
simule un movimiento ondulatorio como el de la Figura 26.34?
26.5.6.
Como ejercicio nal de esta seccin se propone conseguir montar una escena con
una luz y un plano y, a partir de ella, conseguir usar un modelo de iluminacin por
pixel sencillo (como se ve en la Figura 26.17).
En el captulo 9 del Mdulo 2 se habla sobre el tema, a su vez, se puede encontrar
una implementacin, as como una explicacin muy buena sobre iluminacin en el
captulo 5 de The Cg Tutorial [34].
Como pista, o consejo: Se pueden hardcodear (poner como constantes) algunos
parmetros en el material con el objetivo de simplicar la tarea. En el listado 26.29
se muestran todos los que son necesarios para hacer funcionar el ejercicio de luz por
pixel, slo hay que declararlos usando param_named.
Listado 26.29: Ejemplo de declaracin de parmetros para ser usados como uniform por un
shader
1 default_params
2 {
[765]
3
4
5
6
7
8
9
10
11
12
13
14
15 }
26.6.
Las optimizaciones no slo aumentan el nmero de frames por segundo, sino que puede hacer interactivo
algo que no lo sera sin ellas.
param_named
param_named
param_named
param_named
param_named
param_named_auto
eyePosition camera_position_object_space
param_named_auto
lightPosition
light_position_object_space 0
param_named_auto
lightColorDiffuse
light_diffuse_colour 0
Optimizacin de interiores
26.6.1.
Introduccin
C26
Ms velocidad?
param_named
[766]
26.6.2.
Tcnicas y Algoritmos
En su libro, Cormen et all [21] presentan a los algoritmos como una tecnologa
ms a tener en cuenta a la hora de disear un sistema. Es decir, no slo el hardware
ser determinante, sino que la eleccin de un algoritmo u otro resultar determinante
en el buen rendimiento del mismo. Los videojuegos no son una excepcin.
A continuacin se presentarn algunas de las tcnicas y algoritmos que recopila
Dalmau [24] en su libro, que se ha seguido para preparar esta y la siguiente leccin,
para la representacin de escenas de interiores.
Eleccin crucial
Conocer diferentes algoritmos y saber elegir el ms apropiado en cada momento es de suma importancia. Un buen algoritmo puede suponer la diferencia entre poder disfrutar de una aplicacin interactiva o
no poder hacerlo.
Dado que esta complejidad es demasiado alta para una aplicacin interactiva, se
hace necesario idear alguna forma para reducir el nmero de clculos. Una forma
sencilla es reducir la lista de posibles oclusores. Los tringulos ms cercanos a la
cmara tienen ms posibilidades de ser oclusores que aquellos que estn ms alejados,
ya que estadsticamente estos van a ocupar ms rea de la pantalla. De este modo,
usando estos tringulos como oclusores y los lejanos como ocluidos, se podr reducir
la complejidad hasta casi O(n). Si adems se tiene en cuenta que el frustum de la
cmara tiene un lmite (plano far), se podrn descartar aun ms tringulos, tomando
como ocluidos los que estn ms all que este plano.
Este algoritmo podra beneciarse del clipping y del culling previo de los
tringulos. No es necesario incluir en el algoritmo de oclusores ningn tringulo que
quede fuera del frustum de la vista actual, tampoco los tringulos que formen parte de
las caras traseras se pintarn, ni tendrn que formar parte de los oclusores. La pega es
que hay que realizar clipping y culling por software. Tras esto, se tendran que ordenar
los tringulos en Z y tomar un conjunto de los n primeros, que harn de oclusores
en el algoritmo. Computacionalmente, llevar a cabo todas estas operaciones es muy
caro. Una solucin sera hacerlas slo cada algunos frames, por ejemplo cuando la
cmara se moviese lo suciente. Mientras esto no pasase, el conjunto de oclusores no
cambiara, o lo hara de manera mnima.
Listado 26.31: Actualizacin de los oclusores
1 const size_t maxOccluders = 300;
2
3 newPos = calculateNewCameraPosition();
Recursividad
Como casi todos los algoritmos de
construccin de rboles, el BSP es
un algoritmo recursivo. Se dice que
una funcin es recursiva cuando se
llama a s misma hasta que se cumple una condicin de parada. Una
funcin que se llama a s misma dos
veces se podr representar con un
rbol binario; una que se llame nveces, con un rbol n-ario.
[767]
4 if (absoluteDifference(oldPos, newPos) > delta)
5 {
6
performClipping(sceneTriangles);
7
performCulling(sceneTriangles);
8
oldPos = newPos;
9
vector<Triangles> occluders =
10
getZOrderedTriangles(sceneTriangles,
11
maxOccluders);
12 }
Algoritmo BSP
Una de las formas para facilitar la representacin de escenas interiores en tiempo
real es el uso de estructuras de datos BSP (Binary Space Partition). Estas estructuras
se han utilizado desde juegos como Doom, que usaba un arbol BSP de 2 dimensiones,
y Quake, que fue el primero que us uno de 3 dimensiones.
Un BSP es un rbol que se utiliza para clasicar datos espaciales, ms concretamente tringulos en la mayora de los casos. La ventaja principal de esta estructura es
que se le puede preguntar por una serie de tringulos ordenados por el valor Z, desde cualquier punto de vista de la escena. Estos rboles se usan tambin para detectar
colisiones en tiempo real.
Construccin de estructuras BSP
Un BSP se construye a partir de un conjunto de tringulos, normalmente de la
parte esttica de una escena, esto es, el mapa del nivel. El algoritmo de construccin
es recursivo:
Figura 26.35: Posicin con respecto a un plano de normal n.
6. Dividir el conjunto total en dos nuevos conjuntos segn queden delante o detrs
de la divisin.
El nivel queda dividido en dos nuevos conjuntos, detrs de la divisin quedan cinco
planos (1.1, 6, 5.1, 8 y 9), y delante tambin (1.2, 2, 3, 4 y 5.2).
C26
7. Para cada uno de los nuevos conjuntos que aun tengan tringulos (ms que un
umbral mximo de divisin dado), volver al paso 2.
[768]
[769]
La solucin pasa por tomar los vrtices del tringulo atravesado como segmentos
y hallar el punto de interseccin de los mismos con el plano. Con esos puntos ser
trivial reconstruir los tringulos resultantes.
La estructura de rbol BSP podra estar representada en C++ como en el listado
siguiente:
Listado 26.32: Class BSP
1
2
3
4
5
6
7
8
9
10
class BSP {
public:
BSP(vector<Triangle> vIn);
private:
BSP* front;
BSP* back;
Plane p;
Triangle t; // vector<Triangle>vt;
};
C26
[770]
10
11
12
13
14
15
16
17
18
19
20
21
22
23 }
La funcin anterior pinta los tringulos (incluidos los que quedan detrs de la
vista) en orden, desde el ms lejano al ms cercano.
Esto era muy til cuando el hardware no implementaba un Z-buffer, ya que est
funcin obtena los tringulos ordenados con un coste linear.
Si cambiamos el algoritmo anterior (le damos la vuelta) recorreremos las caras
desde las ms cercanas a las ms lejanas. Esto s puede suponer un cambio con el
hardware actual, ya que si pintamos el tringulo cuyo valor va a ser mayor en el Zbuffer, el resto de los tringulos ya no se tendrn que pintar (sern descartados por el
hardware).
Clipping Jerrquico
Un BSP se puede extender para usarlo como un sistema de aceleracin de clipping,
quitando los tringulos que queden fuera del frustum de la cmara. Lo nico que
hay que aadir en el rbol durante su construccin en una bounding box por cada
nodo. Cuanto ms se profundice en el rbol, ms pequeas sern, y si el algoritmo
de equilibrado de la divisin es bueno, una bounding box contendr otras dos de un
volumen ms o menos parecido, equivalente a la mitad de la contenedora.
El algoritmo para recorrer el rbol es muy parecido al anterior, y bastara con
introducir una pequea modicacin.
[771]
Listado 26.35: BSPClipping
1 void paintBSP(BSP* bsp, const Viewpoint& vp, const Camera& cam) {
2
if ( isNodeInsideFrustum(bsp, cam.getCullingFrustum()) )
3
{
4
// Igual que en el ejemplo anterior
5
}
6 }
Deteccin de la oclusin
Tambin es posible utilizar un rbol BSP para detectar oclusiones. Este uso se
populariz gracias al motor de Quake, que utilizaba un nuevo tipo de rbol llamado
leafy-BSP, donde se utilizaron por primera vez para el desarrollo de un videojuego. Su
propiedad principal es la de dividir de manera automtica el conjuntos de tringulos
entrante en un array de celdas convexas.
Este nuevo tipo de rboles son BSPs normales donde toda la geometra se ha
propagado a las hojas, en vez de repartirla por todos los nodos a modo de tringulos
divisores. De este modo, en un BSP normal, las hojas slo almacenan el ltimo
tringulo divisor.
Para transformar un BSP en un leafy-BSP lo que hay que hacer es agitar el rbol
y dejar caer los tringulos de los nodos intermedios en las hojas (ver gura 26.42)
Una vez que el rbol se haya generado, se podr almacenar la lista de tringulos
de cada nodo como una lista de celdas numeradas. Para el ejemplo anterior las celdas
se muestran en la gura 26.43.
Figura 26.43: Nivel dividido en
celdas (l-BSP).
C26
[772]
Cada celda representa una zona contigua y convexa del conjunto de tringulos
inicial. Las paredes de las celdas pueden ser o bien reas ocupadas por geometra del
nivel o espacios entre las celdas. Cada espacio abierto debe pertenecer exactamente a
dos celdas.
Ntese que el algoritmo anterior convierte cualquier conjunto de tringulos en una
lista de celdas y de pasillos que las conectan, que es la parte ms complicada.
Es posible precalcular la visibilidad entre las celdas. Para ello se utilizan los
pasillos (o portales, aunque diferentes a los que se vern un poco ms adelante). Se
mandan rayos desde algunos puntos en un portal hacia los dems, comprobndose si
llegan a su destino. Si un rayo consigue viajar del portal 1 al portal 2, signica que
las habitaciones conectadas a ese portal son visibles mutuamente. Este algoritmo fue
presentado por Teller [97].
Esta informacin sobre la visibilidad se almacenar en una estructura de datos
conocida como PVS (Potential Visibility Set), que es slo una matriz de bits de N xN
que relaciona la visibilidad de la la i (celda i) con la de la columna j (celda j).
Rendering
Para representar los niveles de Quake III: Arena se utilizaba un algoritmo ms o
menos como el que se explica a continuacin.
Se comienza determinando en qu celda se encuentra la cmara (el jugador)
utilizando el BSP. Se recorre el rbol desde la raz, comparando la posicin con
la del plano divisor para bajar hasta una hoja y elegir una celda determinada.
Se utiliza el PVS para determinar qu celdas son visibles desde la celda actual,
utilizando la matriz de bits (o de booleanos).
Se renderizan las celdas visibles. Se pintan desde el frente hasta el fondo, lo
que ayudar al Z-buffer a descartar tringulos lo antes posible. Se ordenan las
celdas por distancia, se usa su bounding box para determinar si quedan dentro
del frustum para hacer el clipping de pantalla y se mandan a renderizar.
Como se ha podido ver, gracias al uso de un rbol leafy-BSP se han resuelto
casi todos los problemas de determinacin de la visibilidad utilizando una estructura
precalculada. Esto hace que en el bucle principal del juego no se dedique ningn
esfuerzo a computarla. Adems, este tipo de estructuras son tiles para determinar
colisiones en tiempo real y para ayudar a recorrer los niveles a la IA.
Portal Rendering
Otra de las tcnicas utilizadas para optimizar la representacin de interiores son
los portales (Portals). Es un enfoque diferente a los rboles BSP, pero que ofrece
una aceleracin similar. El motor grco Unreal demostr su validez utilizando una
versin del mismo, y ha ganado adeptos entre los desarrolladores desde entonces.
Permite, al igual que los BSPs, representar slo lo que se ve. En el caso de esta tcnica,
no se precalcula la visibilidad sino que es computada en tiempo real.
Esta tcnica se basa en que los niveles de interiores de un juego estn construidos a
base de habitaciones interconectadas entres s por puertas, por ventanas, o en general,
por portales. Es de aqu de donde viene su nombre. Para representar estas conexiones
entre las diferentes habitaciones ser necesario una estructura de datos de grafo no
!Precalcular es la clave!
Los pasos ms costosos han sido
precalculados, haciendo factible la
representacin en tiempo real.
[773]
dirigido. Cada nodo del grafo es una habitacin y cada vrtice del grafo es un portal.
Ya que es posible que en una habitacin se encuentren varios portales, es necesario
que la estructura de datos permita conectar un nodo con varios otros, o que dos nodos
estn conectados por dos vrtices.
Figura 26.45: Grafo que representa las conexiones del mapa de habitaciones.
Figura 26.44: Mapa de habitaciones conectadas por portales
C26
Portal
[774]
enum portalType {
NORMAL,
MIRROR,
TRANPARENT,
INTERDIMENSIONAL,
BLACK_VOID
};
struct portal {
vector<Vertex3D*> vertexes_;
portalType type_;
Room* room1;
Room* room2;
};
portal
8
//
9
// Invertir la view-matrix
10
//
11
// Pintar la habitacin destino
12
//
13
// Pintar la geometra del portal de forma
14
// translcida con algo de opacidad si se desea
15
break;
16
17 case TRANSPARENT:
18
// Pintar de forma normal la habitacin que corresponda
19
//
20
// Pintar la geometra del portal de forma
21
// translcida con algo de opacidad si se desea
22
break;
23
24 case INTERDIMENSIONAL:
25
// Modificar los vrtices del array con una funcin sinuidal
26
// Pintarlo
27
// Aadir colores chillones a la opacidad del portal.
28
break;
29
30 case BLACK_VOID:
31
// Modificar la geometra para que la habitacin
32
// destino parezca estirada hacia un agujero negro
33
//
[775]
34
35
36 }
Cuando el rectngulo cae en una zona con blanca, es necesario hacer una
comprobacin sobre los valores Z para comprobar si el objeto est delante o detrs.
Esto se consigue con un DEB (Deep Estimation Buffer)2 , que no es ms que un Zbuffer construido por software, utilizando los posibles oclusores. El DEB almacena
la informacin resultante de crear las bounding boxes de los oclusores y almacenar
a modo de pxel el valor ms lejano (al contrario que un Z-buffer normal) para cada
posicin de la pantalla.
El algoritmo completo podra describirse como sigue:
Seleccionar un buen conjunto de oclusores. Se descartarn objetos pequeos,
o con muchos polgonos, y los objetos redundantes. Es posible colocar objetos
falsos que no se pintarn como oclusores a mano, para mejorar el conjunto de
manera premeditada.
En ejecucin, se seleccionan los primeros N oclusores ms cercanos.
2 Buffer
de Estimacin de Profundidad
C26
[776]
Dalmau arma que con esta tcnica se evita pintar de media entre un 40 % y un
60 % de toda la geometra entrante.
Enfoques hbridos
En los videjuegos se suele utilizar la combinacin de tcnicas que ms benecio
brinde al tipo de representacin en tiempo real a la que se est haciendo frente. De
nuevo, Dalmau propone dos aproximaciones hbridas.
Portal-Octree
En un juego donde el escenario principal est repleto de habitaciones y cada una
de ellas est llena de objetos, una aproximacin de travs de un BSP quiz no sera la
mejor idea. No slo porque este tipo de estructuras est pensado principalmente para
objetos estticos, sino porque un rbol BSP suele extenderse muy rpido al empezar a
dividir el espacio.
Si adems el juego requiere que se pueda interactuar con los objetos que hay en
cada habitacin, el BSP queda descartado para almacenar los mismos. Quiz utilizar
la tcnica de portales pueda usarse para las habitaciones, descartando as algo de
geometra del nivel. Aun as la gran cantidad de objetos hara que fueran inmanejables.
Una posible solucin: utilizar portales para representar las habitaciones del nivel,
y en cada habitacin utilizar un octree.
Quadtree-BSP
Hay juegos que poseen escenarios gigantestos, con un rea de exploracin muy
grande. Si se enfoca la particin de este tipo de escenas como la de un rbol
BSP, el gran nmero de planos de divisin har que crezca la geometra de manera
exponencial, debido a los nuevos tringulos generados a partir de la particin.
Una forma de afrontar este problema es utilizar dos estructuras de datos. Una
de ellas se usar para realizar una primera divisin espacial de la supercie (2D, un
Quadtree, por ejemplo) y la otra para una divisin ms exhaustiva de cada una de esas
particiones. De esto modo, se podr utilizar un Quadtree donde cada nodo contiene un
BSP.
De este modo, se pueden utilizar las caractersticas especiales de cada uno de
ellos para acelerar la representacin. En un primer paso, el Quadtree facilitar la
determinacin de la posicin global de una manera muy rpida. Una vez que se sepa en
qu parte del escenario se encuentra la accin, se tomar el BSP asociado a la misma
y se proceder a su representacin como se mostr en el apartado anterior.
Este tipo de representaciones espaciales ms complejas no son triviales, pero a
veces son necesarias para llevar a cabo la implementacin exitosa de un videojuego.
En el siguiente captulo ese introducirn los quadtrees.
Equilibrio
Utilizar lo mejor de cada una de las
tcnicas hace que se puedan suplir
sus debilidades.
[777]
GLuint
GLuint
GLint
GLuint
queries[N];
sampleCount;
available;
bitsSupported;
C26
Hardware
[778]
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
26.6.3.
BSP
El soporte de OGRE para BSP es
histrico. Se usa slo para cargar
mapas de Quake3 y este es el nico casa para el que se recomienda
usarlos.
El futuro de OGRE
Los gestores de escenas de OGRE
estn evolucionando. Utilizar el
gestor por defecto es una buena garanta de que ser compatible con
las siguientes versiones.
[779]
El grafo de escena que se utiliza en OGRE se conoce como scene manager (gestor
de escena) y est representado por la clase Scene-Manager. El interfaz de la misma
lo implementan algunos de los gestores de escena que incluye OGRE, y tambin
algunos otros desarrollados por la comunidad o incluso comerciales.
En OGRE es posible utilizar varios gestores de escena a la vez. De esto modo,
es posible utilizar uno de interiores y de exteriores a la vez. Esto es til por ejemplo
cuando desde un edicio se mira por una ventana y se ve un terreno.
Un gestor de escena de OGRE es responsable entre otras cosas de descartar los
objetos no visibles (culling) y de colocar los objetos visibles en la cola de renderizado.
Interiores en OGRE
El gestor de escena para interiores de OGRE est basado en BSP. De hecho, este
gestor se utiliza con mapas compatibles con Quake 3. Hay dos formas de referirse a
este gestor, la primera como una constante de la enumeracin Ogre::SceneType
(ST_INTERIOR) y otra como una cadena ("BspSceneManager") que se reere
al nombre del Plugin que lo implementa. La forma ms moderna y la preferida es la
segunda.
En la lnea 9 se crea el SceneManager de BSP, y en la lnea 30 se carga el
mapa y se usa como geometra esttica.
Cabe destacar que para poder navegar por los mapas BSP de manera correcta hay
que rotar 90 grados en su vector pitch y cambiar el vector up de la cmara por el eje
Z.
Listado 26.39: Ejemplo de carga de BSP en OGRE
_root = new Ogre::Root();
if(!_root->restoreConfig()) {
_root->showConfigDialog();
_root->saveConfig();
}
Ogre::RenderWindow* window = _root->initialise(true,"BSP");
_sceneManager = _root->createSceneManager("BspSceneManager");
Ogre::Camera* cam = _sceneManager->createCamera("MainCamera");
cam->setPosition(Ogre::Vector3(0,0,-20));
cam->lookAt(Ogre::Vector3(0,0,0));
cam->setNearClipDistance(5);
cam->setFarClipDistance(10000);
// Cambiar el eje UP de la cam, los mapas de Quake usan la Z
// Hacer los ajustes necesarios.
cam->pitch(Ogre::Degree(90));
cam->setFixedYawAxis(true, Ogre::Vector3::UNIT_Z);
Ogre::Viewport* viewport = window->addViewport(cam);
viewport->setBackgroundColour(Ogre::ColourValue(0.0,0.0,0.0));
double width = viewport->getActualWidth();
double height = viewport->getActualHeight();
cam->setAspectRatio(width / height);
loadResources();
[...]
_sceneManager->setWorldGeometry("maps/chiropteradm.bsp");
C26
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
[780]
26.7.
Optimizacin de Exteriores
26.7.1.
Introduccin
Las diferencias entre una escena de interiores y una de exteriores son evidentes.
Mientras una escena de interiores se dar en entornos cerrados, con muchas paredes o
pasillos que dividen en espacio en habitaciones, una escena de exteriores normalmente
no tiene ningn lmite que no est impuesto por la naturaleza. Si bien es cierto que es
una escena de este tipo, por ejemplo, pudiesen existir colinas que se tapasen unas a
las otras, si estamos situados frente a algunas de ellas en una posicin muy lejana, el
nmero de tringulos que se deberan representar sera tan elevado que quiz ningn
hardware podra afrontar su renderizado.
Est claro que hay que afrontar la representacin de exteriores desde un enfoque
diferente: hacer variable el nivel de detalle LOD). De este modo, los detalles de una
montaa que no se veran a cierta distancia no deberan renderizarse. En general, el
detalle de los objetos que se muestran grandes en la pantalla (pueden ser pequeos
pero cercanos), ser mayor que el de los objetos menores.
Si bien el nivel de detalle es importante, tampoco se descarta el uso de oclusiones
en algunos de los algoritmos que se presentarn a continuacin, siguiendo de nuevo
la propuesta de Dalmau. Se comenzar haciendo una pasada rpida por algunas de las
estructuras de datos necesarias para la representacin de exteriores eciente.
26.7.2.
Estructuras de datos
Uno de los principales problemas que se tienen que resolver para la representacin
de exteriores es la forma de almacenar escenas compuestas por grandes extensiones
de tierra.
Las estructuras de datos utilizadas tendrn que permitir almacenar muchos datos,
computar de manera eciente el nivel de detalle necesario y permitir que la transicin
entre diferentes dichos niveles sea suave y no perceptible.
Mapas de altura
Los mapas de altura (heigtelds o heightmaps) han sido utilizados desde hace
mucho tiempo como forma para almacenar grandes supercies. No son ms que
imgenes en las que cada uno de sus pxeles almacenan una altura.
Cuando se empez a usar esta tcnica, las imgenes utilizaban tan solo la escala
de grises de 8-bit, lo que supona poder almacenar un total de 256 alturas diferentes.
Los mapas de altura de hoy en da pueden ser imgenes de 32-bit, lo que permite
que se puedan representar un total de 4.294.967.296 alturas diferentes, si se usa el
canal alpha.
[781]
Para transformar uno de estos mapas en una representacin 3D es necesario hacer
uso de un vector de 3 componentes con la escala correspondiente. Por ejemplo, si se
tiene un vector de escala s = (3, 4, 0,1) quiere decir que entre cada uno los pxeles del
eje X de la imagen habr 3 unidades de nuestra escena, entre los de Y habr 4, y que
incrementar una unidad el valor del pxel signicar subir 0,1 unidades. La posicin
vara segn cmo estn situados los ejes.
Las ventajas de utilizar un mapa de altura es que se pueden crear con cualquier
herramienta de manipulacin de imgenes y que se pueden almacenar directamente
en memoria como arrays de alturas que se transformarn en puntos 3D cuando sea
necesario, liberando de este modo mucha memoria.
Las principal desventaja viene de que cada pxel representa una sola altura,
haciendo imposible la representacin de salientes o arcos. Normalmente todo este
tipo de detalles tendrn que aadirse en otra capa.
Quadtrees
Un quadtree permite que ciertas reas del terreno se puedan representar con ms
detalle puesto que es posible crear rboles no equilibrados, utilizando ms niveles
donde sea necesario. Una aproximacin vlida es comenzar con un mapa de altura y
crear un quadtree a partir del mismo. Las partes del escenario donde exista ms detalle
(en el mapa de altura, habr muchas alturas diferentes), se subdividirn ms veces.
Figura 26.50: Divisin de un terreno con un quadtree (Q IUHUA
L IANG)
C26
Un quadtree es un rbol donde cada nodo tendr exactamente cuatro hijos, as, se
dice que es un rbol 4-ario. Un quadtree divide un espacio en cuatro partes iguales por
cada nivel de profundidad del rbol (gura 26.49).
[782]
26.7.3.
Determinacin de la resolucin
[783]
Polticas Discretas de LOD
Si se utiliza una poltica discreta, se tendrn varias representaciones del mismo
objeto, con diferentes nivel de detalle. Este nivel de detalle ir desde la versin
original, con el mayor detalle posible, a una versin con muy pocos tringulos. Se
crear la versin de alto detalle y se generarn versiones simplicadas reduciendo el
nmero de tringulos gradualmente con alguna herramienta de diseo 3D.
Teniendo una tabla con diferentes modelos donde elegir, el algoritmo de pintado
simplemente tiene que elegir el que corresponda y ponerlo en la cola de renderizado.
El problema de estas polticas es que existen un momento en el que se produce un
cambio notable en el objeto, y es completamente perceptible si no se disimula de
alguna forma.
Una de las tcnica que se usa para ocultar el salto que se produce al cambiar
de modelo es utilizar alpha blending entre el modelo origen y destino. Este efecto
se puede ver como un cross-fade entre los mismos, cuya intensidad depender del
heurstico utilizado. As en la mitad de la transicin, cada modelo se renderizar
con un alpha de 0.5 (o del 50 %). Justo antes de empezar la transicin, el modelo
origen tendr un valor alpha de 1 y el destino de 0, y al nalizar tendrn los valores
intercambiados. Un inconveniente muy importante de esta tcnica es que durante un
tiempo durante el cual antes slo se representaba un objeto, ahora se representarn
dos, lo que supone una sobrecarga de la GPU.
Polticas Continuas de LOD
Si se quiere evitar del todo el salto producido por el intercambio de modelos, se
podra implementar una forma de reducir el nmero de tringulos en tiempo real, y
generar un modelo dependiendo de la resolucin requerida.
Este tipo de clculos en tiempo real son muy costosos, porque hay que determinar
qu aristas, vrtices o tringulos se pueden eliminar y adems aplicar esa modicacin
al modelo.
Hoppe [44] propone una implementacin eciente de lo que llama mallas progresivas (Progressive Meshes). La tcnica se basa en la eliminacin de aristas de la malla,
convirtiendo dos tringulos en slo uno por cada arista eliminada (edge-collapsing).
Hoppe determina que esta tcnica es suciente para simplicar mallas y propone algunos heursticos para eliminar las aristas.
Hay dos posibles aproximaciones a la hora de quitar una arista, la primera, crear
un nuevo vrtice en el centro de la misma, y la otra eliminarla completamente (ms
eciente). La primera aproximacin es vlida para todas las aristas, la segunda es slo
vlida para aristas que corten a tringulos y no a cuadrilteros, puesto que al eliminarla
se debera obtener un polgono con tres vrtices.
Este tipo de polticas permite obtener el mejor resultado, a costa de aadir un coste
computacional. Adems, otra desventaja muy importante es que la informacin de
mapeado de las texturas del objeto se podr ver afectada por la reduccin del nmero
de tringulos.
Figura 26.52: Edge-Collapsing.
Arriba, el strip original. Abajo, el
nuevo strip despus de aplicar la
tcnica.
C26
Un posible heurstico para utilizar es el ngulo que forman los dos tringulos
conectados por dicha arista. Si el ngulo es menor que un umbral, se quitar esa arista.
Como optimizacin, esta tcnica no debera utilizarse en cada frame, sino slo cuando
cambie la distancia o el rea que ocupa el objeto en pantalla lo suciente.
[784]
26.7.4.
Tcnicas y Algoritmos
Una ventaja de utilizar este tipo de representacin es que los pedazos de malla se
pueden mandar como una sola primitiva (strips) el hardware. La desventaja es que los
vrtices de los 4 bordes de dichos trozos se comparten con los bloques que lo rodean,
y se transformarn dos veces.
View-Frustum Culling
Ser necesario descartar los pedazos de terreno que no caigan dentro del frustum
de la vista (cmara) puesto que no sern visibles. Para ello, lo ideal es utilizar un
quadtree.
El quadtree ser precalculado antes de comenzar la parte interactiva de la
aplicacin y consistir tan solo en bounding boxes de tres dimensiones, que a su vez
contendrn otras correspondientes a los subnodos del nodo padre. En cada hoja del
rbol quedar un pedazo del nivel 0 de divisiones que se ha visto al principio. Es
suciente utilizar quadtrees y no octrees ya que la divisin se realiza de la supercie,
y no del espacio.
Para descartar partes del terreno, se recorrer el rbol desde la raz, comprobando
si la bounding box est dentro del frustum al menos parcialmente, y marcando dicho
nodo en caso armativo. Si una hoja est marcada, quiere decir que ser visible y que
se mandar a la cola de representacin. A no ser que el terreno sea muy pequeo,
se terminarn mandando muchos tringulos a la cola, y esta optimizacin no ser
suciente. Es aqu donde De Boer introduce el concepto de Geomipmapping.
[785]
Geomipmaps y nivel de detalle
Esta tcnica se basa en el hecho de que los bloques que estn ms lejos de la
cmara no necesitan representarse con tan nivel de detalle como los ms cercanos.
De este modo, podrn ser representados con un nmero mucho menor de tringulos,
lo que reducir enormemente el nmero de tringulos del terreno que se mandarn a
la cola de renderizado. Otro algortmos utilizan una aproximacin en la que hay que
analizar cada tringulo para poder aplicar una poltica de nivel de detalle. Al contrario,
esta tcnica propone una poltica discreta que se aplicar en un nivel ms alto.
La tcnica clsica de mipmapping se aplica a las texturas, y consiste en la
generacin de varios niveles de subtexturas a partir de la original. Este conjunto de
texturas se utilizan en una poltica de nivel de detalle para texturas, usando unas u
otras dependiendo de la distancia a la cmara. Esta es la idea que se va a aplicar a la
mallas 3D de terrenos.
Cada bloque de terreno tendr asociado varios mipmaps, donde el original corresponde al bloque del mapa de altura. Estos GeoMipMaps pueden ser precalculados y
almacenados en memoria para poder ser utilizados en tiempo de ejecucin directamente.
Figura 26.56: Diferentes niveles de detalle de la malla desde los GeoMipMaps : niveles 0, 1 y 2.
C26
En la gura 26.57 se muestra una propuesta que consiste en conectar los vrtices
de la malla de nivel superior con los de la de nivel inferior pero saltando un vrtice
cada vez.
[786]
ROAM
De este modo se construye un rbol que representa la malla, donde cada nivel
del mismo corresponde a un conjunto de tringulos que pueden ser encolados para
su renderizado. El rbol podr expandirse en tiempo real si fuera necesario. Bajar un
nivel en el rbol equivale a una operacin de particin y subir a una de unin.
El problema viene de la unin de regiones triangulares con diferente nivel de
detalle, ya que si no se tienen en cuenta aparecern agujeros en la malla representada.
Mientras que en el geomipmapping se parchea la malla, en ROAM, cuando se detecta
una discontinuidad se utiliza un oversampling de los bloques vecinos para asegurar
que estn conectados de manera correcta. Para ello, se aade informacin a cada lado
de un tringulo y se utilizan alguna reglas que garantizarn la continuidad de la malla.
Se conoce como vecino base de un tringulo al que est conectado a este a travs
de la hipotenusa. A los otros dos tringulos vecinos se los conocer como vecino
izquierdo y derecho (gura 26.59). Analizando diferentes rboles se deduce que el
vecino base ser del mismo nivel o del anterior (menos no) nivel de detalle, mientras
que los otros vecinos podrn ser del mismo nivel o de uno ms no.
[787]
Las reglas propuestas para seguir explorando el rbol y evitar roturas en la malla
sern las siguientes:
Si un nodo es parte de un diamante, partir el nodo y el vecino base.
Si se est en el borde de una malla, partir el nodo.
Si no forma parte de un diamante, partir el nodo vecino de forma recursiva hasta
encontrar uno antes de partir el nodo actual (gura 26.60).
El algoritmo en tiempo de ejecucin recorrer el rbol utilizando alguna mtrica
para determinar cundo tiene que profundizar en la jerarqua, y cmo ha de realizar
las particiones cuando lo haga depender de las reglas anteriores.
Realizar en tiempo real todos estos clculos es muy costoso. Para reducir este
coste, se puede dividir el terreno en arrays de BTTs y slo recalcular el rbol cada
ciertos frames y cuando la cmara se haya movido lo suciente para que cambie la
vista por encima de un umbral.
Otra aproximacin para optimizar el algoritmo es utilizar el frustum para determinar qu nodos hay que reconstruir de nuevo, marcando los tringulos que quedan
dentro o fuera, o parcialmente dentro. Slo estos ltimos necesitarn atencin para
mantener el rbol actualizado. A la hora de representarlo, lo nico que habr que hacer es mandar los nodos marcados como dentro, total o parcialmente.
Chunked LODs
Ulrich [100] propone un mtodo para representar grandes extensiones de tierra. En
la demo del SIGGRAPH 2002 incluye un terreno que cubre 160Km2 . Este mtodo
tiene su partida en una imagen muy grande, por ejemplo obtenida de un satlite, lo
que hace el mtodo ideal para la implementacin de simuladores de vuelo.
Para almacenar la informacin se utilizar un quadtree. Se comenzar con una
imagen potencia de dos en el nodo raz, que corresponder a una imagen de muy baja
resolucin del terreno completo. Segn se vaya profundizando, los 4 subnodos hijos
contendrn imgenes del mismo tamao pero con la calidad resultante de hacer un
zoom a los cuatro cuadrantes de la misma (gura 26.61).
Figura 26.61: Nodo y subnodos de las texturas en un quadtree para Chunked LODs.
A cada uno de los nodos se le aade informacin acerca de la prdida del nivel de
detalle se produce al subir un nivel en la jerarqua. Si las hojas contienen imgenes de
32x32 pxeles, los pedazos de terreno contendrn 32x32 vrtices. El rbol de mallas
tambin forma parte del preproceso normalmente, partiendo de una malla muy grande
y con mucha resolucin y partindola en trozos ms pequeos.
C26
[788]
[789]
Mientras que para almacenar un terreno y las capas estticas que lleva encima es
muy buena opcin utilizar un quadtree, ser mejor utilizar alguna tabla de tipo rejilla
para almacenar la posicin de los objetos dinmicos para determinar cules de ellos
se pintar en cada fotograma de manera rpida.
26.7.5.
CG Plugin
Para compilar el plugin CgManager
de OGRE en Debian, habr que instalar antes nvidia-cg-toolkit, que es
privativo.
C26
[790]
En la lineas 3-5 se congura niebla en
usando un color oscuro para
la escena,
hacer que parezca oscuridad. En las lneas 7-14 se congura una luz direccional que
se utilizar
en el mismo. En
en el terreno. A partir de ella se calcularn las sombras
la lnea 16 se congura la luz ambiental de la escena. En la 19 se crea un nuevo
objeto de conguracin del terreno, que se rellenar ms adelante. En la 20 se crea
un objeto agrupador de terrenos. Este objeto es el responsable de agrupar diferentes
pedazos
del terreno juntos para que puedan ser representados como proceda. En la
lnea 26 se congura el error mximo medido
en pxeles de pantalla que se permitir
al representar el terreno. En las lneas 28-31 se congura la textura que compodr con
l, dibujando las sombras. Primero se determina la distancia de representacin de las
luces, luego se selecciona la direccin de las sombras, que parte de una luz direccional
y luego se conguran el valor ambiental y difuso de iluminacin.
Listado 26.40: Ejemplo de terreno en OGRE. Basado en O GRE 3D W IKI
1 void MyApp::createScene()
2 {
3
_sMgr->setFog(Ogre::FOG_LINEAR,
4
Ogre::ColourValue(0.1, 0.1, 0.1),
5
0.5, 2000, 5000);
6
7
Ogre::Vector3 lightdir(0.55, -0.3, 0.75);
8
lightdir.normalise();
9
10
Ogre::Light* light = _sMgr->createLight("DirLight");
11
light->setType(Ogre::Light::LT_DIRECTIONAL);
12
light->setDirection(lightdir);
13
light->setDiffuseColour(Ogre::ColourValue::White);
14
light->setSpecularColour(Ogre::ColourValue(0.4, 0.4, 0.4));
15
16
_sMgr->setAmbientLight(Ogre::ColourValue(0.2, 0.2, 0.2));
[791]
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
=
=
=
=
=
513;
12000.0f;
600;
33;
65;
di.layerList.resize(3);
di.layerList[0].worldSize = 100;
di.layerList[0].textureNames.push_back("dirt_diff_spec.png");
di.layerList[0].textureNames.push_back("dirt_normal.png");
di.layerList[1].worldSize = 30;
di.layerList[1].textureNames.push_back("grass_diff_spec.png");
di.layerList[1].textureNames.push_back("grass_normal.png");
di.layerList[2].worldSize = 200;
di.layerList[2].textureNames.push_back("growth_diff_spec.png");
di.layerList[2].textureNames.push_back("growth_normal.png");
Ogre::Image im;
im.load("terrain.png",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
long x = 0;
long y = 0;
Ogre::String filename = _tGroup->generateFilename(x, y);
if (Ogre::ResourceGroupManager::getSingleton().resourceExists(
_tGroup->getResourceGroup(), filename))
{
_tGroup->defineTerrain(x, y);
}
else
{
_tGroup->defineTerrain(x, y, &im);
_tsImported = true;
}
_tGroup->loadAllTerrains(true);
if (_tsImported)
{
Ogre::TerrainGroup::TerrainIterator ti;
ti = _tGroup->getTerrainIterator();
while(ti.hasMoreElements())
{
Ogre::Terrain* t = ti.getNext()->instance;
initBlendMaps(t);
}
}
_tGroup->freeTemporaryResources();
C26
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
52
53
54
55
56
57
58
59
60
61
62
[792]
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
}
void MyApp::initBlendMaps(Ogre::Terrain* t)
{
Ogre::TerrainLayerBlendMap* blendMap0 = t->getLayerBlendMap(1);
Ogre::TerrainLayerBlendMap* blendMap1 = t->getLayerBlendMap(2);
Ogre::Real
Ogre::Real
Ogre::Real
Ogre::Real
minHeight0
fadeDist0
minHeight1
fadeDist1
=
=
=
=
70;
40;
70;
15;
}
}
blendMap0->dirty();
blendMap1->dirty();
blendMap0->update();
blendMap1->update();
En 34 se obtiene la instancia que congura algunos parmetros del terreno.
El primero es el tamao del terreno, que corresponde al nmero de pxeles del
mapa de altura. El segundo el tamao total del mundo, en unidades virtuales. El
tercero (inputScale) corresponde a la escala aplicada al valor del pxel, que se
transformar en la altura del mapa. Las dos siguientes corresponden al tamao de
los bloques de terrenos que se incluirn en la jerarqua (diferentes LOD). Estos tres
ltimos valores tendrn que ser del tipo 2n + 1. El atributo layerList contiene un
vector de capas, que se rellenar con las texturas (color y mapa de normales en este
caso). El atributo
worldSize corresponde a la relacin de tamao entre la textura y
el mundo. En 55 se carga la imagen con el mapa de altura.
Las siguientes lneas son las que asocian el mapa de altura con el terreno que se va
a generar. En este ejemplo slo se genera
elsubterreno (0,0) con lo que slo se cargar
una imagen. En las siguientes lneas 61-70 se dene el terreno, en este caso, slo para
el slot (0,0), aunque el ejemplo est preparado para ser extendido fcilmente y aadir
la denicin algunos ms. La llamada defineTerrain() expresa la intencin de
crear ese pedazo de terreno con la imagen
que contiene el mapa de altura. La ejecucin
de este deseo se realiza en la linea 72 . Sin esta llamada, el ejemplo no funcionara
puesto que se ejecutara el siguiente paso sin haber cargado (generado) el terreno.
[793]
En el bloque 74-83 se crea la capa de blending, esto es, la fusin entre las tres
texturas del terreno (habr dos
de blending). Esta operacin se realiza en el
mapas
mtodo initBlendMaps 96-124 . Dependiendo de la altura a la que corresponda
un pxel de las imgenes a fusionar (el tamao ha de ser coherente con el mapa de
altura) as ser el valor de blending aplicado, con lo que se conseguir que cada altura
presente una textura diferente.
C26
[794]
}
}
Como cabra esperar, la skybox se congura para el gestor de escenas. sta se encontrar
a una distancia ja de la cmara (el tercer parmetro), y podr estar activada o no (primer
parmetro).
Ejemplo de SkyDome
Aunque slo se utiliza una textura, realmente una SkyDome est formada por las cinco
caras superiores de un cubo. La diferencia con una skybox es la manera en la que se proyecta
la textura sobre dichas caras. Las coordenadas de texturizado se generan de forma curvada y
por eso se consigue tal efecto de cpula. Este tipo de objetos es adecuado cuando se necesita
un cielo ms o menos realista y la escena no va a contener niebla. Funcionan bien con texturas
repetitivas como las de nubes. Una curvatura ligera aportar riqueza a una escena grande y una
muy pronunciada ser ms adecuada para una escena ms pequea donde slo se vea pedazos
de cielo de manera intermitente (y el efecto exagerado resulte atractivo).
Un ejemplo de material es el siguiente:
material SkyDomeCEDV
{
[...]
texture_unit
{
texture clouds.jpg
scroll_anim 0.15 0
}
[...]
}
setSkyBox()
Esta llamada acepta ms parmetros, si fuera necesario denir una
orientacin tendra que usarse un
quinto parmetro (el cuarto es un
booleano que indica que la caja se
dibuja antes que el resto de la escena.
[795]
Donde el tercer parmetro es la curvatura (funciona bien para valores entre 2 y
65), y el cuarto es el nmero de veces que se repite la textura en la cpula.
Ejemplo de SkyPlane
Un skyplane es a priori la ms simple de las representaciones. Como su nombre
indica est basado en un plano, y la creacin de uno es necesaria. Aun as tambin es
posible aplicarle algo de curvatura, siendo ms adecuado para escenas con niebla que
una cpula. El material podra ser el mismo que antes, incluso sin utilizar animacin.
Se ve un ejemplo del uso del mismo en el ejemplo anterior del terreno.
Se propone crear una escena con cada uno de las tres tcnicas anteriores. Si
fuera posible, utilice el ejemplo de terreno anterior.
Materiales
En los materiales no slo se remite a las texturas sino a todas las propiedades
editables dentro del bloque technique. Para congurar un material con soporte para
LOD lo primero es congurar la estrategia (lod_strategy) que se va a utilizar de
las dos disponibles:
Distance - Basado en la distancia desde la cmara hasta la representacin del
material. Se mide en unidades del mundo.
Tras esto, lo siguiente es determinar los valores en los que cambiar el nivel de detalle para dicha estrategia. Se usar para tal labor la palabra reservada lod_values
seguida de dichos valores. Es importante destacar que tendrn que existir al menos
tantos bloques de technique como valores, ya que cada uno de ellos estar relacionado con el otro respectivamente. Cada uno de los bloques technique debe tener
asociado un ndice para el nivel de detalle (lod_index). Si no aparece, el ndice ser el 0 que equivale al mayor nivel de detalle, opuestamente a 65535 que corresponde
con el menor. Lo normal es slo tener algunos niveles congurados y probablemente
jams se llegue a una cifra tan grande. Aun as, es importante no dejar grandes saltos y hacer un ajuste ms o menos ptimo basado en las pruebas de visualizacin de
la escena. Es posible que varias tcnicas tengan el mismo ndice de nivel de detalle,
siendo OGRE el que elija cul es la mejor de ellas segn el sistema en el que se est
ejecutando.
Un ejemplo de material con varios niveles de detalle:
material LOD_CEDV
C26
[796]
{
lod_values 200 600
lod_strategy Distance
technique originalD {
lod_index 0
[...]
}
technique mediumD {
lod_index 1
[...]
}
technique lowD {
lod_index 2
[...]
}
technique shaders {
lod_index 0
lod_index 1
lod_index 2
[...]
}
}
Como ejercicio se propone construir una escena que contenga 1000 objetos
con un material con tres niveles de detalle diferente. Siendo el nivel 0 bastante
complejo y el nivel 3 muy simple. Muestre los fotogramas por segundo y
justique el uso de esta tcnica. Se sugiere mostrar los objetos desde las
diferentes distancias conguradas y habilitar o deshabilitar los niveles de
detalle de baja calidad.
Modelos
OGRE proporciona tres formas de utilizar LOD para mallas. La primera es una
donde modelos con menos vrtices se generan de forma automtica, haciendo uso
de progressiveMesh internamente. Para utilizar la generacin automtica ser
necesario que la malla que se cargue no tenga ya de por s LOD incluido (por ejemplo,
la cabeza de ogro de las demos ya contiene esta informacin y no se podr aplicar).
Una vez que la entidad est creada, se recuperar la instancia de la malla contenida
dentro.
[797]
Listado 26.41: Generacin de niveles de detalle para una malla
1 Entity *entidad = sceneManager->createEntity("objeto", "objeto.mesh
");
2
3
4
5
6
7
8
9
10
11
12
En la lnea 3 del ejemplo anterior se crea una lista de distancias para el
nivel de detalle. Esta lista no es ms que un vector de reales que determina
los valores que se utilizarn segn la estrategia elegida para esa entidad. Por
defecto corresponde con la distancia y en ese caso los valores corresponden a las
races cuadradas de la distancia en la que se producen cambios. La estrategia se
puede cambiar con setLodStrategy() usando como parmetro un objeto de
tipo base lodStrategy, que ser DistanceLodStrategy o PixelCountLodStrategy. Estas dos clases son singletons, y se deber obtener su instancia
utilizando getSingleton(). La segunda de ellas corresponde con la poltica
basada en el nmero de pxeles que se dibujan
la pantalla de una esfera que contiene
en
al objeto (bounding-sphere). En la lnea 12 se llama a la funcin que genera los
diferentes niveles a partir de la malla original. El segundo parmetro es corresponde
con el mtodo que se usar para simplicar la malla:
VRQ_CONSTANT - Se elimina un nmero dijo de vrtices cada iteracin.
VRQ_PROPORTIONAL - Se elimina una cantidad proporcional al nmero de
vrtices que queda en la malla.
El tercero corresponde con el valor asociado al mtodo y en el caso de ser un
porcentaje tendr que estar acotado entre 0 y 1.
La segunda forma de aadir LOD a una entidad de OGRE es hacerlo de manera
completamente manual. Ser necesario crear diferentes mallas para el mismo objeto.
Esto se puede llevar a cabo en un programa de edicin 3D como Blender. Se empieza
con el modelo original, y se va aplicando algn tipo de simplicacin, guardando
cada uno de los modelos obtenidos, que sern usados como mallas de diferente nivel
de detalle para la entidad que represente a dicho modelo.
Listado 26.42: Generacin de niveles de detalle para una malla
Ogre::MeshPtr m =
Ogre::MeshManager::getSingleton().load("original.mesh",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
m->createManualLodLevel(250, "lod2.mesh");
m->createManualLodLevel(500, "lod3.mesh");
m->createManualLodLevel(750, "lod3.mesh");
Ogre::Entity* objeto = sceneManager->createEntity("objetoConLOD",
m->getName() );
Ogre::SceneNode* n = sceneManager->getRootSceneNode()->
createChildSceneNode();
13 n->attachObject(objeto);
C26
1
2
3
4
5
6
7
8
9
10
11
12
[798]
26.7.6.
Conclusiones
Captulo
27
Plataformas Mviles
Miguel Garca Corchero
Motores de videojuego
Existen otros motores de videojuegos ms utilizados en la industria
como CryEngine o Unreal Engine
y tienen ms funcionalidades que
Unity3D pero tambin es ms complejo trabajar con ellos.
27.1.
27.1.1.
Diseo del videojuego: Esta fase suele hacerse con papel y boli. En ella
deniremos las mecnicas necesarias a implementar, haremos bocetos de los
personajes o situaciones implicadas y creamos listas de tareas a realizar
asignando diferentes prioridades.
799
[800]
27.1.2.
Scripts
Los scripts que utilizamos se pueden escribir en los lenguajes C#, Javascript o BOO.
27.2.
Creacin de escenas
[801]
Figura 27.3: En nuestro ejemplo se han utilizado capturas de pantalla de google map para obtener texturas
de terreno y se han mapeado sobre un plano en Blender. Posteriormente se ha utilizado el modo scuplt para
dar relieve al terreno y generar un escenario tridimensional.
C27
[802]
Figura 27.4: Interface de Unity3D. Dividida en las vistas ms utilizadas: Jerarqua de escena, Assets del
proyecto, Vista del videojuego, Vista de escena y Vista de propiedades del asset seleccionado.
Como las escenas pueden cargarse desde otra escena. Podemos realizar un cambio
de escena cuando se ha llegado al nal de un determinado nivel por ejemplo. Este
cambio de escena mantendr en memoria los elementos comunes como texturas o
modelos 3D o sonidos que pertenezcan a los dos escenas, la escena que se descarga y
la escena que se carga, por lo que dependiendo del caso esta carga suele ser bastante
rpida. Tambin podemos realizar los mens en una escena y desde ah cargar la
escena del primer nivel del juego.
[803]
Figura 27.6: Para la escena de nuestro ejemplo hemos aadido el modelo 3D del escenario, los enemigos,
el jugador y hemos establecido las relaciones de jerarqua necesarias entre estos elementos.
27.3.
Creacin de prefabs
Podemos agrupar esa jerarqua y darle un nombre propio para despus serializarla
e instanciarla en el futuro. A ese concepto se le conoce con el nombre de prefab.
Podemos crear tantos prefabs como queramos a partir de jerarquas de objetos de una
escena y son una parte fundamental para entender el mtodo de trabajo con un motor.
En nuestra escena hemos creado prefabs para cada uno de los tipos de enemigos, y
tambin hemos creado prefabs para el disparo, una explosin y un efecto de partculas
de llamas.
C27
Uso de prefabs
[804]
[805]
27.4.
Programacin de scripts
Una script es un chero de cdigo que contiene instrucciones sobre el comportamiento de un determinado actor de nuestra escena. Podemos aadir uno o varios
scipts a cada uno de los elementos de nuestra escena y adems los scripts tienen la
posibilidad de hacer referencia a estos objetos o scripts de otros objetos.
Documentacin
Podemos consultar la documentacin de cada una de las APIs para
los tres lenguajes de scripting desde
la pgina ocial de Unity3D
En los scripts podemos utilizar las clases y APIs que nos proporciona el motor de
videojuegos. Algunas de estas clases son:
GameObject: Esta clase tiene informacin sobre el objeto. Todos los nodos de
una escena son GameObjects.
Transform: Esta clase representa la posicin, rotacin y escala de un elemento
en el espacio tridimensional.
AudioSource: Esta clase almacena un sonido y permite gestionar su reproduccin.
Texture 2D: Esta clase contiene una textura bidimensional.
Los scripts tienen algunos mtodos especiales que podemos implementar como:
Update: Este mtodo es invocado por el motor grco cada vez que el objeto
va a ser renderizado.
Start: Este mtodo es invocado por el motor grco cuando se instancia el
objeto que contiene a este script.
27.4.1.
#pragma strict
public var delta = 4.0;
function Update () {
//Rotar la helice en el eje y
transform.Rotate(0,Time.deltaTime * delta,0);
}
En nuestro escenario se han aadido nubes modeladas mediante planos y con una
textura de una nube con transparencias. Para darle mayor realismo a este elemento se
va a programar un script para mover las coordenadas u,v del mapeado de este plano
provocando que la textura se mueve sobre el plano y simulando un movimiento de
nubes.
Listado 27.2: Nube.js
1 #pragma strict
2
3 public var delta = 0.1;
4 public var moveFromLeftToRight : boolean = false;
C27
[806]
Triggers
27.4.2.
Un trigger es una porcin del espacio denida por un objeto geomtrico como
una caja o una esfera que utilizaremos para colocar sobre la escena y de esta forma
saber cuando un determinado objeto entra o sale de una zona concreta. De esta forma
podremos invocar diferentes comportamientos en ese momento. En nuestra escena
utilizaremos un trigger para determinar cuando hemos llegado al enemigo nal.
Listado 27.3: TriggerFinal.js
1 #pragma strict
2
3 function OnTriggerEnter (other : Collider) {
4
if (other.tag=="Player"){
5
//Si entra un objeto dentro de el trigger y el objeto es el
6
7
8
9
10 }
27.4.3.
A veces necesitaremos programar un evento para que pase trascurrido un determinado tiempo. En nuestro ejemplo esto ocurre con la explosin, que invocaremos su
destruccin 5 segundos despus de haberse instanciado.
Listado 27.4: DestruirPasadoUnTiempo.js
1 public var timeOut = 1.0;
2
Variables pblicas
Las variables pblicas nos permitirn modicar esos parmetros desde el interface de Unity3D cuando
tengamos el objeto que contiene el
script seleccionado.
[807]
3 function Start(){
4
//Realizar una llamada al mtodo destruir pasados los segundos
de timeOUT
5
Invoke ("Destruir", timeOut);
6 }
7
8 function Destruir(){
9
//Destruir el objeto que contiene este script
10
DestroyObject (gameObject);
11 }
27.4.4.
#pragma strict
var
var
var
var
var
SonidoDisparo : AudioSource;
SonidoExplosionAire : AudioSource;
SonidoExplosionSuelo : AudioSource;
SonidoVuelo : AudioSource;
MusicaJuego : AudioSource;
C27
[808]
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
function PlaySonidoExplosionSuelo(){
SonidoExplosionSuelo.Play();
}
function PlaySonidoExplosionAire(){
SonidoExplosionAire.Play();
}
function PlaySonidoDisparo(){
SonidoDisparo.Play();
}
function PlayMusicaJuego(){
MusicaJuego.Play();
}
function StopMusicaJuego(){
MusicaJuego.Stop();
}
function PlayMusicaFinal(){
MusicaFinal.Play();
}
function StopMusicaFinal(){
MusicaFinal.Stop();
}
function PlaySonidoVuelo(){
SonidoVuelo.Play();
}
27.4.5.
#pragma strict
public
public
public
public
public
var
var
var
var
var
Lmite de FPS
En dispositivos mviles el limite de
frames por segundo est ajustado
por defecto a 30 FPS pero podemos
cambiarlo modicando el atributo
Application.targetFrameRate.
Retardos
Utilizaremos la instruccin WaitForSeconds para introducir retardos
en los scripts.
[809]
20 function Awake(){
21
//Hacemos que el juego corra a 60 FPS como mximo
22
Application.targetFrameRate = 60;
23 }
24
25 function Start(){
26
//Obtenemos el puntero a Sonidos y ajustamos algunos valores
iniciales
var SonidosPointer : Transform = (GameObject.FindWithTag("
Sonidos")).transform;
SonidosStatic = (SonidosPointer.GetComponent("Sonidos") as
Sonidos);
SonidosStatic.PlayMusicaJuego();
SonidosStatic.PlaySonidoVuelo();
ScoreGUI.text="Score : "+Score;
27
28
29
30
31
32 }
33
34 function Update () {
35
if (enZonaFinal && velocidadCamara>0.0){
36
//Si estamos en la zona final paramos el movimiento de
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
manera gradual
velocidadCamara*=0.95;
if (velocidadCamara<0.1) { velocidadCamara=0; }
if (velocidadCamara>0.0){
//Movemos la cmara en su componente z para hacer scroll
Camara.position.z+=Time.deltaTime * velocidadCamara;
}
function EntrarEnZonaFinal(){
//Se ha entrado en el trigger de la zona final
enZonaFinal=true;
SonidosStatic.StopMusicaJuego();
SonidosStatic.PlayMusicaFinal();
function FinDeJuegoGanando(){
//Fin de partida cuando hemos completado la misin
FondoFinal.texture = TexturaFinalBien;
Restart();
}
function FinDeJuegoPerdiendo(){
//Fin de partida cuando hemos fallado la misin
FondoFinal.texture = TexturaFinalMal;
Restart();
}
72
73 function Restart(){
74
//Ocultamos los textos y botones
75
LifeGUI.enabled=false;
76
ScoreGUI.enabled=false;
77
BotonDISPARO.enabled=false;
78
BotonIZ.enabled=false;
79
BotonDE.enabled=false;
80
FondoFinal.enabled=true;
81
82
//Esperamos 5 segundos y hacemos un reload de la escena
83
yield WaitForSeconds (5);
84
Application.LoadLevel(Application.loadedLevel);
85 }
C27
[810]
27.4.6.
Programacin de enemigos
#pragma strict
public
public
public
public
public
public
public
var
var
var
var
var
var
var
explosionPrefab : Transform;
llamasPrefab : Transform;
disparoPrefab : Transform;
player : Transform;
rangoDisparo : float;
tiempoRecarga = 0.5;
jefeFinal : boolean = false;
Deteccin de colisiones
Cuando un objeto tridimensional
tiene aadidos los elementos collider y rigidbody permite detectar colisiones mediante el mtodo OnCollisionEnter.
[811]
13
14 private var llamasInstancia : Transform;
15 private var cantidadRotationCaida : Vector3 = Vector3(0,0,0);
16
17 function Update () {
18
if (transform.gameObject.rigidbody.useGravity){
19
//Si el enemigo est callendo, el avin rota sobre sus ejes
20
21
22
23
24
25
26
27
28
29
30
31
32
33 }
34
35 function OnCollisionEnter(collision : Collision) {
36
//Determinar posicin y rotacin del punto de contacto de la
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
colisin
var contact : ContactPoint = collision.contacts[0];
var rot : Quaternion = Quaternion.FromToRotation(Vector3.up,
contact.normal);
var pos : Vector3 = contact.point;
var SonidosPointer : Transform = (GameObject.FindWithTag("
Sonidos")).transform;
var SonidosStatic : Sonidos = (SonidosPointer.GetComponent("
Sonidos") as Sonidos);
if (transform.gameObject.rigidbody.useGravity || collision.
collider.tag=="Player"){
//Si estamos callendo y hemos vuelto a colisionar entonces
explota
var ControlJuegoPointer : Transform = (GameObject.
FindWithTag("ControlJuego")).transform;
var ControlJuegoStatic : ControlJuego = (
ControlJuegoPointer.GetComponent("ControlJuego") as
ControlJuego);
SonidosStatic.PlaySonidoExplosionSuelo();
//Instanciamos la explosin final en la posicin del
impacto
Instantiate(explosionPrefab, pos, rot);
if (llamasInstancia!=null){
Destroy (llamasInstancia.gameObject);
}
if (jefeFinal) {
ControlJuegoStatic.AddScore(500);
ControlJuegoStatic.FinDeJuegoGanando();
} else {
var cantidadScore : float = (rangoDisparo * (1.0/
tiempoRecarga))*5;
ControlJuegoStatic.AddScore(cantidadScore);
}
C27
37
38
[812]
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 }
27.4.7.
#pragma strict
public
public
public
public
public
public
public
public
public
public
public
private
private
private
private
private
var
var
var
var
var
var
var
var
var
var
var
explosionPrefab : Transform;
disparoPrefab : Transform;
posicionDisparoIZ : Transform;
posicionDisparoDE : Transform;
tiempoRecarga = 0.5;
cantidadMovimiento = 0.1;
camara : Transform;
vida : int = 5;
LifeGUI : GUIText;
topeIZ : Transform;
topeDE : Transform;
var
var
var
var
var
siguienteTiempoDisparo = 0.0;
anteriorDisparoIZ : boolean = false;
botonIzquierda : boolean = false;
botonDerecha : boolean = false;
botonDisparo : boolean = false;
function Start(){
//Inicializamos el marcador de vida con el valor de vida
inicial
23
LifeGUI.text="Life : "+vida;
24 }
25
26 function Update() {
27
if ((botonDisparo || Input.GetButton("Fire1")) && Time.time >
28
29
30
siguienteTiempoDisparo){
//Si hay que disparar, instanciamos prefabs de disparo en
las posiciones alternativamente izquierda y derecha de
el avin del jugador
siguienteTiempoDisparo = Time.time + tiempoRecarga;
if (anteriorDisparoIZ){
Teclas de control
Aunque el juego nal se controle
mediante botones virtuales dibujados sobre la pantalla tctil del dispositivo, tambin permitiremos su
control con un teclado para cuando
probemos el juego en el emulador
integrado en Unity3D.
[813]
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Instantiate(disparoPrefab, posicionDisparoDE.position,
posicionDisparoDE.rotation);
} else {
Instantiate(disparoPrefab, posicionDisparoIZ.position,
posicionDisparoIZ.rotation);
}
anteriorDisparoIZ=!anteriorDisparoIZ;
if (botonIzquierda || Input.GetButton("Left")){
//Si hay moverse a la izquierda se actualiza la posicin
del jugador
//Tambin se mueve un poco la cmara para simular un poco
de efecto parallax
if (transform.position.x>topeIZ.position.x) {
transform.position.x-= Time.deltaTime *
cantidadMovimiento;
camara.position.x-= Time.deltaTime * cantidadMovimiento
/2;
}
} else if (botonDerecha || Input.GetButton("Right")) {
//Si hay moverse a la derecha se actualiza la posicin del
jugador
//Tambin se mueve un poco la cmara para simular un poco
de efecto parallax
if (transform.position.x<topeDE.position.x) {
transform.position.x+= Time.deltaTime *
cantidadMovimiento;
camara.position.x+= Time.deltaTime * cantidadMovimiento
/2;
}
}
51
52
53 }
54
55 function OnCollisionEnter(collision : Collision) {
56
if (collision.collider.tag=="DisparoEnemigo" || collision.
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
collider.tag=="Enemigo"){
//Si el jugador colisiona con un disparo o un enemigo la
vida disminuye
vida--;
LifeGUI.text="Life : "+vida;
if (vida<=0){
//Si la vida es 0 entonces acaba la partida
var ControlJuegoPointer : Transform = (GameObject.
FindWithTag("ControlJuego")).transform;
var ControlJuegoStatic : ControlJuego = (
ControlJuegoPointer.GetComponent("ControlJuego") as
ControlJuego);
ControlJuegoStatic.FinDeJuegoPerdiendo();
//Reproducimos sonido de explosin
var SonidosPointer : Transform = (GameObject.
FindWithTag("Sonidos")).transform;
var SonidosStatic : Sonidos = (SonidosPointer.
GetComponent("Sonidos") as Sonidos);
SonidosStatic.PlaySonidoExplosionSuelo();
//Instanciamos un prefab de explosin en la posicin
del avin del jugador
Instantiate(explosionPrefab, transform.position,
Quaternion.identity);
//Eliminamos el avin del jugador
Destroy(gameObject);
74
75
76
}
77
}
78 }
79
80
81 //Mtodos para controlar la pulsacin de los botones virtuales
C27
57
[814]
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
function ActivarBotonIzquierda(){
botonIzquierda=true;
}
function ActivarBotonDerecha(){
botonDerecha=true;
}
function ActivarBotonDisparo(){
botonDisparo=true;
}
function DesactivarBotonIzquierda(){
botonIzquierda=false;
}
function DesactivarBotonDerecha(){
botonDerecha=false;
}
function DesactivarBotonDisparo(){
botonDisparo=false;
}
27.4.8.
Utilizaremos botones dibujados sobre la pantalla tctil para controlar el videojuego. Para colocar cada uno de los botones virtuales utilizaremos un script que en
funcin del valor del atributo tipoGUI lo posicionar en una zona determinada de la
pantalla.
Listado 27.10: ColocarGUI.js
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
#pragma strict
enum TipoGUI { Life, Score, BotonLeft, BotonRight, BotonShoot };
public var tipoGUI : TipoGUI;
function OnGUI () {
// Hacemos que el ancho del botn ocupe un 10 por ciento
// Alto del botn mantiene la proporcin respecto a la imagen
var anchoBoton : float = Screen.width*0.1;
var altoBoton : float = anchoBoton * 94.0/117.0;
var margen : int = 10;
//Dependiendo del tipo de guiTexture o guiText; colocamos
switch(tipoGUI){
case tipoGUI.Life:
guiText.pixelOffset = Vector2 (Screen.width/2 - 55,
Screen.height/2 - margen);
break;
case tipoGUI.Score:
guiText.pixelOffset = Vector2 (-Screen.width/2 + margen
, Screen.height/2 - margen);
break;
case tipoGUI.BotonLeft:
guiTexture.pixelInset = Rect (-Screen.width/2 + margen,
-Screen.height/2 + margen, anchoBoton, altoBoton);
break;
case tipoGUI.BotonRight:
guiTexture.pixelInset = Rect (-Screen.width/2 +
anchoBoton+ 2*margen, -Screen.height/2 +margen,
anchoBoton, altoBoton);
break;
OnGUI
El mtodo OnGui ser llamado
cuando se redimensiona la ventana
o se cambia de resolucin de modo
que se calcule la posicin de los elementos con respecto a las dimensiones de la pantalla para que siempre
estn bien colocados.
[815]
27
28
29
30
31 }
case tipoGUI.BotonShoot:
guiTexture.pixelInset = Rect (Screen.width/2 anchoBoton - margen, - Screen.height/2 + margen,
anchoBoton, altoBoton);
break;
#pragma strict
public
public
public
public
var
var
var
var
tipoGUI : TipoGUI;
Boton : GUITexture;
TextureON : Texture2D;
TextureOFF : Texture2D;
21
22
23
24
25
26
27
28
29 function Activate() {
30
//Ponemos la textura botn pulsado
31
Boton.texture= TextureON;
32
//Dependiendo del tipo de botn que pulsamos enviamos el
33
switch(tipoGUI){
34
case tipoGUI.BotonLeft:
35
PlayerStatic.ActivarBotonIzquierda();
36
break;
37
case tipoGUI.BotonRight:
38
PlayerStatic.ActivarBotonDerecha();
39
break;
40
case tipoGUI.BotonShoot:
41
PlayerStatic.ActivarBotonDisparo();
42
break;
43
}
44 }
45
46 function Deactivate() {
47
//Ponemos la textura botn sin pulsar
48
Boton.texture= TextureOFF;
49
//Dependiendo del tipo de botn que soltamos enviamos el
50
51
C27
[816]
52
case tipoGUI.BotonLeft:
53
PlayerStatic.DesactivarBotonIzquierda();
54
break;
55
case tipoGUI.BotonRight:
56
PlayerStatic.DesactivarBotonDerecha();
57
break;
58
case tipoGUI.BotonShoot:
59
PlayerStatic.DesactivarBotonDisparo();
60
break;
61
}
62 }
63
64 function Start () {
65
//Obtenemos el puntero a Player y ajustamos algunos valores
66
67
68
69
70 }
27.5.
iniciales
var PlayerPointer : Transform = (GameObject.FindWithTag("Player
")).transform;
PlayerStatic = (PlayerPointer.GetComponent("Player") as Player)
;
wasClicked = false;
Boton.texture= TextureOFF;
Optimizacin
27.5.1.
Light mapping
Esta tcnica consiste en calcular previamente las sombras que reciben y provocan
los objetos estticos de las escenas para generar unos mapas de sombreado que se
aplican mediante multitextura sobre la maya de los modelos 3D. Los modelos sobre
los que se aplica esta tcnica son renderizados como polgonos con textura sin ningn
sombreado, lo que evita el calculo de iluminacin de la maya, ahorrando mucho
tiempo de computo.
27.5.2.
Occlusion culling
Esta tcnica consiste en calcular previamente desde todas las posibles posiciones
que puede tomar la cmara que objetos son visibles y cuales son ocluidos por otros.
Despus se utiliza esta informacin en tiempo de renderizado para no representar los
objetos que despus no van a ser visibles.
El clculo de todas las posiciones que puede tomar la cmara se hace discretizando
el espacio mediante una matriz tridimensional de la cual podremos elegir el nivel de
granularidad. De este modo se calcula que objetos estticos debern ms tarde ser
renderizados cuando la cmara se encuentre en esta regin del espacio.
La combinacin de esta tcnica junto con otras como frustrum culling permitirn
que podamos tener escenas con millones de polgonos, pero en cada frame de
renderizado slo se representar una pequea fraccin de estos polgonos.
[817]
Figura 27.14: Ejemplo de las sombras que proyectan sobre el terreno los modelos tridimensionales de unas
ruinas colocados en la escena.
C27
27.5. Optimizacin
Figura 27.16: Ejemplo de mapa generado mediante esta tcnica que despus se mapear sobre el modelo
3D de la escena. Este mapa de sombreado es de la zona que se aprecia en la gura 27.18.
[818]
27.6.
Resultado nal
El resultado nal es una pieza jugable de unos dos minutos y medio de duracin
que podra ser un nivel de un videojuego shoot em up de aviones con vista cenital.
Este videojuego est listo para compilarse para dispositivos con iOS o Android
y funcionar en terminales de gama media-baja, pudiendo alcanzar los 60 FPS en
terminales de gama alta.
[819]
Figura 27.19: En nuestro ejemplo se ha dividido el escenario en porciones para poder aplicar la tcnica, de
este modo en un momento determinado slo se renderiza una pequea parte del escenario.
C27
Figura 27.20: Nivel de granularidad elegido para la matriz de discretizacin del espacio en nuestro ejemplo.
Desarrollo de Componentes
El objetivo de este mdulo, titulado Desarrollo de Componentes
dentro del Curso de Experto en Desarrollo de Videojuegos, consiste
en profundizar en tcnicas especficas vinculadas al desarrollo de
videojuegos, como por ejemplo el uso de tcnicas de Inteligencia
Artificial o la programacin multijugador en red. Para ello, una de
las principales metas es la de complementar la visin general de la
arquitectura de un motor de juegos con cuestiones especficas que
resultan fundamentales para su desarrollo. Dentro del contexto de la
Inteligencia Artificial, en este mdulo se estudian tcnicas
fundamentales como la Lgica Difusa o los algoritmos genricos,
entre otras. As mismo, se realiza una discusin del diseo orientado
a agentes como pilar esencial en el desarrollo del componente
inteligente de un videojuego. En la parte relativa al juego
multijugador se exploran las posibilidades que ofrecen los sockets y,
posteriormente, se discute cmo el uso de herramientas de ms alto
nivel, como los middlewares de comunicaciones pueden contribuir a
facilitar el desarrollo del mdulo de networking. Finalmente, este
mdulo tambin contempla aspectos relativos a la edicin de audio,
la gestin de vdeo y la importancia de la integracin de nuevos
dispositivos de interaccin. En el contexto del desarrollo de
videojuegos, tcnicas como la visin por computador o la realidad
aumentada pueden contribuir a mejorar la experiencia del jugador.
Captulo
28
Inteligencia Articial
Javier Alonso Albusac Jimnez
Jos Jess Castro Snchez
David Vallejo Fernndez
Luis Jimnez Linares
Manuel Palomo Duarte
[824]
28.1.
28.1.1.
Figura 28.2: Alan Turing (19121954), matemtico, cientco, criptgrafo y lsofo ingls, es considerado uno de los Padres de la
Computacin y uno de los precursores de la Informtica Moderna.
28.1.2.
[825]
Ilusin de inteligencia
C28
En [16], el autor discute el caso de la IA del juego Halo, desarrollado por Bungie
Studios y publicado por Microsoft Games Studio en 2001, y cmo los desarrolladores
consiguieron engaar a los testers del juego. En concreto, los desarrolladores
asociaban el nivel de IA con la altitud de los puntos de impacto sobre los NPCs. As,
los jugadores perciban un grado bajo de IA cuando este nivel no era elevado, es decir,
cuando los impactos en la parte inferior del NPC eran relevantes para acabar con el
mismo. Sin embargo, al incrementar dicho nivel y forzar a los jugadores a apuntar a
partes ms elevadas del NPC, stos perciban que el juego tena una IA ms elevada.
[826]
#ifndef __PLAYER__
#define __PLAYER__
class Player {
public:
Player (int stamina, float height);
~Player () {}
void reduceStamina (int value) {
if ((_stamina - value) > 0)
_stamina -= value;
else
destroy();
}
int getStamina () const { return _stamina; }
int getHeight () const { return _height; }
private:
int _stamina; // Vida del personaje.
float _height; // Altura.
void destroy () {}
};
AIBehaviour
ormalAI
Behaviour
HardAI
Behaviour
#endif
#ifndef __AIBEHAVIOUR__
#define __AIBEHAVIOUR__
#include <OgreVector3.h>
#include "Player.h"
class AIBehaviour {
public:
AIBehaviour (Player* player) { _player = player; }
// El impacto de hit depender del nivel de AI.
virtual void hit (const Ogre::Vector3& bullet) = 0;
protected:
Player* _player;
};
class NormalAIBehaviour: public AIBehaviour {
public:
NormalAIBehaviour (Player* player): AIBehaviour(player) {}
void hit (const Ogre::Vector3& bullet);
};
class HardAIBehaviour: public AIBehaviour {
public:
HardAIBehaviour (Player* player) : AIBehaviour(player) {}
void hit (const Ogre::Vector3& bullet);
};
#endif
5 7 8
10
8 7 5
[827]
La implementacin de la funcin miembro hit() de las clases derivadas NormalAIBehaviour y HardAIBehaviour diere en funcin del punto de impacto del proyectil
sobre el personaje y, consecuentemente, en la reduccin de su nivel de stamina.
Listado 28.3: Implementacin de la funcin miembro hit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <AIBehaviour.h>
// SE OBVIA LA DETECCIN DE COLISIONES.
void
NormalAIBehaviour::hit
(const Ogre::Vector3& bullet)
{
_player->reduceStamina(25.0);
}
// Considera la altura del impacto de la bala.
void
HardAIBehaviour::hit
(const Ogre::Vector3& bullet)
{
float aux = _player->getHeight() / 3.0;
float top = _player->getHeight() - aux;
Academic VS Game AI
Recuerde que en el mbito acadmico, las tcnicas de IA tienden
a buscar soluciones ptimas, mientras que en el contexto de los videojuegos la tendencia general consiste
en encontrar buenas soluciones.
28.1.3.
NPCs o Agentes?
C28
IA cooperativa
[828]
Agente
Sensores
?
Actuadores
Medio o contexto
Percepciones
Acciones
[829]
accin
siguiente
estado
Entorno
Figura 28.8: Visin abstracta del funcionamiento interno de un agente.
28.1.4.
El modelo de diseo de las mquinas de estados est basado en el tradicional esquema de accin y reaccin. Bsicamente, ante el cumplimiento de una condicin (accin) se
produce una transicin (reaccin).
Fuzzy Logic
La lgica difusa o borrosa es una
tcnica que permite tratar la incertidumbre y la vaguedad presenta en la
mayora de los problemas del mundo real. sta se puede utilizar para
proporcionar ms exibilidad a un
esquema basado en mquinas de estados.
C28
Accin, reaccin
[830]
after (30s)
Inactivo
Patrullando
ruido
Rastreando
sin enemigos
Atacando
enemigo
detectado
28.1.5.
[831]
C28
[832]
#!/usr/bin/python
# -*- coding: utf-8 -*import sys, random
jugadas = []
def jugar ():
while True:
try:
total_humano = int(input(\nTotal? ))
n_maquina, total_maquina = predecir(total_humano)
n_humano = int(input(Cuantas tenias? ))
print \nHumano: + str(n_humano)
print Maquina: + str(n_maquina)
if (n_maquina + n_humano) == total_humano:
print Ganaste!
elif (n_maquina + n_humano) == total_maquina:
print Perdiste!
# Para el mdulo de IA...
jugadas.append((n_humano, total_humano))
except KeyboardInterrupt:
print \nSaliendo...
sys.exit(0)
def predecir (total_humano):
n_maquina = random.randint(0, 3)
total_maquina = random.randint(n_maquina, n_maquina + 3)
print La maquina predice + str(total_maquina)
return n_maquina, total_maquina
jugar()
El anterior listado de cdigo contiene una funcin predecir que debera implementar una IA ms sosticada, ms all de devolver valores aleatorios. Una posible opcin
sera utilizar la estructura de datos jugadas para generar algn tipo de patrn que modelara el comportamiento del humano despus de varias rondas de juego. Se plantea
como ejercicio al lector la implementacin de la funcin predecir() con el objetivo
de estudiar la secuencia previa de predicciones por parte del jugador humano, con el
objetivo de ganar un mayor nmero de partidas.
28.1.6.
[833]
5 5
5 5
4 4 4 4
3
22 2
2 2
2 2
11 1 1 1 1 1 1 1 1 1
(a)
(b)
x
x
x
(c)
Figura 28.11: Planteando un mdulo de IA para el Tetris. a) Limpieza de una lnea, b) Cuanticando la
altura del montn de chas, c) Problema de huecos perdidos (los puntos representan los huecos mientras
que las x representan los potenciales bloqueos).
C28
Para ello, una opcin bastante directa consiste en distinguir qu aspectos resultan
fundamentales para ganar o perder una partida. En principio, el mdulo de IA debera
evitar formar torres de chas de gran altura, ya que lo aproximaran a perder la partida
de forma inminente, tal y como muestra la gura 28.11.b. Este factor debera suponer
una penalizacin para la puntuacin asociada a colocar una cha. Por el contrario, la
mquina debera limpiar lneas siempre que fuera posible, con el objetivo de obtener
puntos y evitar que el montn de chas siga creciendo. Este factor representara una
bonicacin.
[834]
(28.1)
donde:
salt representa la suma de las alturas asociada a la pieza a colocar en una
determinada posicin,
nclears representa el nmero de lneas limpiadas,
nh representa el nmero de huecos generados por la colocacin de la cha en
una determinada posicin,
nb representa el nmero de bloqueos como consecuencia de la potencial
colocacin de la cha.
wi representa el peso asociado al factor i.
Evidentemente, nclears tendr asociado un peso positivo, mientras que el resto
de factores tendrn asociados pesos negativos, ya que representan penalizaciones a la
hora de colocar una cha.
Una inconveniente inmediato de esta primera aproximacin es que no se contempla, de manera explcita, la construccin de bloques consistentes por parte del mdulo
de IA de la mquina. Es decir, sera necesario incluir la lgica necesaria para premiar
el acoplamiento entre chas de manera que se premiara de alguna forma la colocacin
lgica de las mismas. La gura 28.13 muestra un ejemplo grco de esta problemtica, cuya solucin resulta fundamental para dotar a la mquina de un comportamiento
inteligente.
28.2.
Tcnicas Fundamentales
Figura 28.13: Idealmente, la posicin 2 debera ser premiada en detrimento de la posicin 1, ya que facilita la colocacin de futuras piezas.
[835]
Una de las principales caractersticas potenciales de este tipo de algoritmos es que
son adaptativos; permiten encontrar soluciones en entornos dinmicos o cambiantes,
incluso si existe un gran desconocimiento inicial sobre el problema a tratar. Esta
caracterstica los hace especialmente adecuados en el desarrollo de videojuegos de
ltima generacin en donde se busca el mayor realismo posible, y se intenta modelar
entornos y comportamientos poco predecibles.
Por otro lado, las redes neuronales tambin estn basadas en la propia naturaleza.
Se trata de algoritmos de aprendizaje y razonamiento basados en el sistema nervioso
de los animales (interconexin de neuronas en una red que colabora para producir
un estmulo de salida). Las redes neuronales y los algoritmos genticos se suelen
combinar con frecuencia, ya que los segundos permiten construir el espacio de casos
o entrenamiento que las redes neuronales necesitan para su correcto funcionamiento.
28.2.1.
Lgica Difusa
Por otro lado, el segundo principio enuncia que la lgica difusa se basa principalmente en el estudio de los grados de pertenencia a los conjuntos difusos denidos.
Normalmente, un sistema difuso consta de un conjunto de variables de entrada; cada
una de estas variables posee un dominio de denicin (rango de valores que las va-
C28
Figura 28.14: Profesor Lofti Zadeh, padre de la lgica difusa, present su teora en el ao 1965 en la
revista Information and Control.
Por ejemplo, supongamos el caso en el que dos personas se estn pasando una
pelota. El lanzador no es consciente en ningn momento, del ngulo que forma su
brazo con el cuerpo, ni la fuerza exacta que realiza para lanzar la bola. Uno de ellos
podra pedir al otro que le lanzara la pelota ms fuerte. El trmino lingstico ms
fuerte es impreciso en s; en ningn momento se especica cuanto debe aumentar
el valor de la fuerza para cumplir el objetivo deseado. La persona encargada de
lanzar, puede resolver el problema rpidamente, aumentando la fuerza con respecto
al lanzamiento anterior sin la necesidad de realizar clculos precisos. Esta forma de
trabajar, en la que se manejan trminos lingsticos, conceptos vagos e imprecisos
es un tanto opuesta a la forma en la que el computador trabaja. Sin embargo,
puede ofrecer soluciones rpidas y ecientes a problemas reales, con un bajo coste
computacional.
[836]
riables pueden tomar) en el que se incluyen los conjuntos difusos. Segn el valor de
entrada de la variable, ste pertenecer a uno o varios conjuntos del dominio con un
cierto grado de pertenencia. Esta es una de las principales caractersticas que diferencian a la lgica difusa de la lgica tradicional.
En la lgica bivaluada existen dos valores de verdad posibles: verdadero y falso.
En la lgica difusa pueden existir valores intermedios, es decir, un hecho podra ser
no del todo cierto y no del todo falso. En cuanto a la teora de conjuntos tradicional,
un elemento pertenece absolutamente a un conjunto o no pertenece. En el caso de
la lgica difusa, un elemento puede pertenecer a diferentes conjuntos con distintos
grados. Los grados de pertenencia pertenecen al intervalo [0,1], donde 1 representa
pertenencia absoluta y 0 el caso contrario. Por tanto, podemos decir que la lgica
difusa es una generalizacin de la lgica clsica.
Veamos un ejemplo; supongamos que un sistema posee una variable de entrada
llamada altura. El objetivo de este sistema podra ser catalogar a un conjunto de
personas adultas como muy bajo, bajo, mediano, alto o muy alto. Supongamos que la
altura puede variar entre 1.40 y 2.20. En el dominio de denicin de la variable altura
habr que indicar de algn modo la correspondencia entre los trminos muy bajo,
bajo, mediano, alto y muy alto, y el rango posible de valores numricos [1.40-2.20].
Supongamos que en un sistema clsico, se considera personas bajas hasta 1.69 y las
personas medianas desde 1.70 hasta 1.75. En este sistema, una persona que mida 1.69
sera clasicada como baja, mientras que una persona que mida 1.70 sera clasicada
como mediana.
Tal como se puede apreciar, la discriminacin en el proceso de clasicacin entre
una persona y otra es excesiva teniendo en cuenta que la diferencia es mnima (slo
un centmetro). La lgica difusa contempla la pertenencia a varios conjuntos al mismo
tiempo, es decir, una persona que mida 1.70 podra pertenecer al conjunto de los
medianos con un grado 0.6 mientras que al conjunto de los bajos podra ser 0.4. Este
es un ejemplo orientativo para concebir una idea general sobre la esencia en la que
se basa la lgica difusa; en secciones posteriores se explicar con mayor detalle la
denicin de conjuntos difusos y el clculo de grados de pertenencia.
[837]
Sistemas de escritura.
Mejora de la eciencia en el consumo de combustibles en vehculos.
Sistemas expertos que simulan el comportamiento humano.
Tecnologa informtica.
E1
E2
E3
E4
0
V
E1
E2
E3
0
V
E1
E2
E3
0
V
C28
[838]
Proceso de Fuzzificacin
Entrada crisp Entrada difusa
Motor de inferencia
Proceso de inferencia Salida difusa
Proceso de defuzzificacin
Salida difusa Salida crisp
Figura 28.19: Esquema general de un sistema difuso.
[839]
En la Figura 28.20 se puede observar un ejemplo en el que se dene un espacio
difuso para la variable altura. Cada uno de los conjuntos difusos se dene mediante
un trapezoide y tiene asociado una etiqueta lingstica. Para denir cada uno de los
trapecios y sus lmites son necesarios cuatro valores numricos. Tal como se puede
apreciar, el valor de la altura vara desde 1.40 hasta 2.20 y todos los casos son
abarcados por los conjuntos difusos denidos.
Los dominios de una variable se pueden variar fcilmente modicando los lmites
de los conjuntos; esta caracterstica dota a los sistemas difusos de gran exibilidad. El
ejemplo que se muestra en la Figura 28.20 podra servir para clasicar los estudiantes
de un curso en una universidad espaola. Suponga que quisiramos clasicar los
alumnos de una universidad noruega en la que la mayora de alumnos supera la medida
de 1.80 metros. En este caso, una persona que mida 1.80 puede ser considerado en ese
contexto con una estatura media y no alta. Para tal caso, bastara con modicar los
lmites de los conjuntos sin la necesidad de alterar el resto de partes del sistema.
Altura
Muy bajo
bajo
medio
alto
muy alto
0
1.40
1.45
1.55
1.60
1.65
1.70
1.75 1.80
1.85
2.20
Figura 28.20: Dominio de denicin o espacio difuso denido para la variable altura.
Una vez denidas las variables y sus dominios de denicin, el siguiente paso
consiste en la fuzzicacin de variables a medida que reciban valores de entrada. Este
paso consiste bsicamente en el estudio de los valores de pertenencia a los conjuntos
difusos en funcin de los valores crisp de entrada. Por ejemplo, supongamos que una
persona mide 1.64, es decir, altura = 1.64. Dicho valor de altura estara comprendido
entre los conjuntos bajo y medio, con una pertenencia mayor al conjunto medio.
Un espacio difuso est correctamente denido, si la suma de todas las pertenencias
a los conjuntos siempre es 1, independientemente del valor de entrada. Formalmente,
la denicin de la funcin de pertenencia asociada a un trapecio o conjunto difuso
trapezoidal es de la siguiente manera:
u<a
au<b
buc
c<ud
u>d
(28.2)
C28
(ua)
(ba)
1
(u; a, b, c, d) =
(du)
(dc)
[840]
Altura
Muy bajo
bajo
medio
alto
muy alto
0
1.40
1.45
1.55
1.60
1.65
1.70
1.75 1.80
1.85
2.20
Figura 28.21: Estudio de los valores de pertenencia a los conjuntos difusos a partir de una altura de 1.64.
|DDVi |
j=1
2
3 {
4
5
6
7
8
9
10
11
12
13
14
15 }
double result = 0;
if (a <= u
result
else if (b
result
else if (c
result
&& u < b )
= (u-a)/(b-a);
<= u && u <= c)
= 1;
< u && u <= d)
= (d-u)/(d-c);
return result;
[841]
8
9
10
11
12
13
14 }
else if (u
result
else if (b
result
== b)
= 1;
< u && u <= c)
= (c-u)/(c-b);
return result;
Motor de inferencia
Una vez que se ha realizado el proceso de fuzzicacin, para determinar el
grado de pertenencia de las variables a los conjuntos difusos, comienza el proceso
de inferencia o razonamiento para la toma de decisiones. Uno de los motores de
inferencia ms comunes es el basado en reglas difusas de tipo SI-ENTONCES. Este
tipo de reglas est formado por un conjunto de antecedentes conectados mediante
operadores lgicos (AND, OR, NOT) y uno o varios consecuentes. Cada antecedente
a su vez consiste en una variable difusa y el rango de etiquetas lingsticas que sta
debera tomar para que se produzca la activacin de la regla. Veamos un ejemplo:
C28
[842]
Mtodo
Directo
Modelado Difuso
Takagi y Sugeno
Razonamiento
Aproximado
Mtodo
Indirecto
Mtodo Directo
de Mandani
Mtodo
Simplificado
[843]
A1
B1
C1
w1
0
x
A2
y
B2
z
C2
w2
x0
y0
z
C1
C2
z
z0 (centroide)
z Z
z Z
C28
[844]
Proceso de defuzzicacin
El proceso de defuzzicacin es necesario slo si se desea un valor real o crisp
de salida en nuestro sistema (valor z0 en la gura 28.23). El proceso ms sencillo y
ampliamente utilizado se conoce como mtodo de centro de gravedad, en el que se
obtiene el punto central del rea generada en el consecuente de la regla.
Si tenemos en cuenta de nuevo el ejemplo anterior, donde se plantea el mtodo de
razonamiento de Mandani, los mtodos de defuzzicacin ms comunes son:
Tomar el centro de masas del conjunto difuso conclusin:
z0 =
c (z)z dz
c (z) dz
E - Enemigo
J - Jugador
A - Aliado
E
E
J
A
[845]
Tal como se vio en las las secciones anteriores, existen diferentes alternativas para
denir los conjuntos difusos correspondientes a las etiquetas lingsticas especicadas
en el listado anterior: trapezoidales, triangulares, mediante curvas, etc. Queda a
eleccin del lector, elegir una de las opciones en funcin del comportamiento del
sistema deseado.
Por otro lado, las variables de salida son las que se incluirn en el consecuente
de las reglas y determinarn las acciones bsicas de cada individuo: acercarse, huir,
atacar y defenderse. A continuacin se detalla una breve descripcin sobre cada una
de ellas:
Atacar: Si un enemigo ataca y el otro no se encuentra en posicin de defensa,
recibir el 100 % de dao producido por el ataque. A medida que la energa
del individuo disminuya, el ataque debera producir un dao menor. Por el
contrario, si el individuo se encuentra en posicin de defensa, podra evitar el
dao producido por el ataque (no siempre).
Defenderse: Esta accin permite al individuo que la realiza recuperar parte de
su energa. Adems, podra evitar daos en el caso de recibir un ataque.
Huir: Desplazarse una casilla en el tablero incrementando la distancia con
respecto al enemigo.
Acercarse: Desplazarse una casilla en el tablero disminuyendo la distancia con
respecto al enemigo.
Una vez especicadas las variables de entrada/salida y denido el dominio
difuso para cada una de ellas, el siguiente paso consiste en construir una base
de conocimiento formada por reglas difusas que van a determinar las conductas.
En funcin de la estrategia que queramos adoptar, se debe dar preferencia en el
antecedente de las reglas a unas variables u otras. Por ejemplo, podramos optar por
atacar siempre a aquellos enemigos que tengan una energa ms baja o bien, podramos
optar por atacar al ms cercano. Algunos ejemplos de reglas difusas podran ser los
siguientes:
Si nalmente optamos por realizar una de las cuatro acciones posibles en cada
turno: acercarse, huir, atacar o defenderse; tan slo sera necesario estudiar la
activacin de las reglas y actuar segn aquella que tenga un grado de pertenencia
mayor.
Una vez implementando el sistema difuso, sera interesante realizar modicaciones del dominio de denicin de cada variable para poder observar como vara el
comportamiento de los personajes sin la necesidad de variar la lgica de programa.
La inclusin de nuevas reglas o modicacin de las existentes, tambin contribuye al
cambio de conductas.
C28
[846]
28.2.2.
Algoritmos genticos
Adaptabilidad
Es la principal caracterstica de los
algoritmos genticos; permiten encontrar soluciones que se adecuan a
un entorno.
Operadores evolutivos
Los operadores evolutivos bsicos
son la seleccin, cruce y mutacin.
Los AG son tambin realmente apropiados para conseguir conguraciones ptimas. Imagnese, por ejemplo, el juego Gran Turismo 4 en el que para cada circuito
existen coches y caractersticas mecnicas que se adaptan mejor para lo obtencin
de mejores tiempos. Si se tiene en cuenta que existen ms de 500 coches y docenas de caractersticas congurables, encontrar una conguracin ptima es realmente
complicado. Un AG podra simular mltiples conguraciones y calcular el benecio
proporcionado por cada una de ellas.
Esquema general de un algoritmo gentico
Como se coment anteriormente, los AG constan de un conjunto de procedimientos de bsqueda adaptativos. El primer requisito para un AG es la constitucin de una
poblacin inicial formada por un conjunto de cromosomas. La generacin de dicha
poblacin se puede producir de forma absolutamente aleatoria, o bien, combinndola
con posibles soluciones candidatas que sean conocidas. Independientemente del mtodo de generacin, la poblacin inicial debe poseer diversidad estructural para tener
una representacin de la mayor parte de la poblacin y no caer en una convergencia prematura. La formacin de la primera generacin es vital para llegar con xito a
soluciones prometedoras en las futuras generaciones.
Diversidad Estructural
En un algoritmo gentico es fundamental que la poblacin inicial o
primera generacin posea diversidad estructural para converger a soluciones adecuadas
[847]
Una vez que se ha constituido la primera poblacin o primera generacin, se
procede a la ejecucin de un proceso iterativo. Este proceso se basa en el estudio
de la calidad de cada una de las soluciones mediante el uso de una funcin de aptitud;
es decir, esta funcin informar sobre lo buena que es una solucin. Despus de
estudiar la aptitud de cada solucin o cromosoma, comienza un proceso de seleccin,
de tal forma que los cromosomas elegidos sern combinados para dar lugar a los
individuos de la siguiente generacin. Los cromosomas con mejor aptitud tienen
siempre ms probabilidades de ser seleccionados.
Seleccin de individuos
NO
Solucin optima?/
Condicin de parada
SI
Espacio de soluciones final
C28
Funcin de aptitud
[848]
Figura 28.28: Juego clsico de comecocos en el que los cuatro movimientos o estados posibles del personaje principal son: movimiento
hacia arriba, abajo, izquierda y derecha
Binaria
Enteros
[849]
Operadores de seleccin
Seleccin elitista: se escogen los miembros ms aptos en cada generacin.
Este mtodo se puede entender como un proceso de clonacin, en el que se
copian directamente los mejores miembros de una generacin a la siguiente, de
tal forma que se garantiza que no se pierdan en el proceso de bsqueda. Sin
embargo, abusar de este mtodo no es conveniente ya que la bsqueda puede
converger a soluciones que representan mximos locales y podran evitar la
generacin de una solucin que represente un mximo global.
Seleccin proporcional a la aptitud: Los individuos ms aptos tienen mayor
probabilidad de ser seleccionados pero no la certeza. Esta probabilidad se podra
calcular en funcin del valor de aptitud y de la suma totales de aptitudes:
pi = fi / j fj . Para determinar si un cromosoma se selecciona para una
posterior reproduccin, se puede generar un nmero aleatorio entre 0 y 1; si
el valor generado es inferior a pi , entonces el individuo es seleccionado.
0.125
0.125
0.5
C28
0.25
[850]
Mutacin
La mutacin consiste en alterar de forma aleatoria una o varias partes de la
solucin. En el caso de la naturaleza las mutaciones pueden llegar a ser letales, sin
embargo, contribuyen a la diversidad gentica de la especie. No es conveniente utilizar
con una frecuencia alta este operador para no reducir el AG a una bsqueda aleatoria;
tan slo es aconsejable cuando el algoritmo est estancado. Una buena frecuencia de
mutacin es 1/100 o 1/1000.
Para cada uno de los genes que constituyen una solucin se genera un nmero
aleatorio, entre 0 y 1; si ese nmero es inferior a 0.01 o 0.001 se realiza el intercambio
del valor.
Cruce (Crossover)
Este es el operador principal en un AG y el que se utiliza con mayor frecuencia.
Consiste en el intercambio gentico entre dos cromosomas; para ello se escogen dos
de los miembros elegidos en el proceso de seleccin y se intercambian algunas de las
partes de la cadena formada por los genes. Existen diferentes tipos de operadores de
cruce:
Original 1
Mutado
Procesamiento paralelo
Se exploran varias soluciones de
forma simultnea
[851]
Padre
Madre
Descendientes
Padre
Madre
Descendientes
Padre
Madre
Descendientes
Una de las grandes ventajas de los AG es que estos tienen descendencia mltiple
y pueden explorar el espacio de soluciones de forma paralela. A diferencia de los
algoritmos que buscan una solucin siguiendo un nico camino, los AG no se ven
realmente penalizados en el caso de que uno de los caminos no llegue a una solucin
satisfactoria. Gracias a esta ventaja, los AG tienen un gran potencial para resolver
problemas cuyo espacio de soluciones es grande o para resolver problemas no lineales.
C28
[852]
Otra ventaja importante es que los AG son apropiados para manejar mltiples
parmetros simultneamente. Es posible, que el AG no encuentre una solucin ptima
global y pueden existir varias soluciones al problema en cuestin y que algunas de
ellas optimicen algunos de los parmetros de entrada y otras soluciones, optimicen
otros distintos.
Adems, los AG no tienen porque conocer nada sobre el problema a resolver.
De hecho, el espacio inicial de soluciones puede ser generado aleatoriamente en su
totalidad, y que estos individuos converjan hacia soluciones vlidas para el problema.
Esta caracterstica es muy importante y hace que los AG sean realmente tiles para
resolver problemas en el que se posee un gran desconocimiento sobre las posibles
soluciones y no sera posible denir una de ellas de forma analtica.
Mltiples parmetros
Manejo simultneo de mltiples parmetros de entrada, hasta encontrar la conguracin adecuada
Desconocimiento
Representacin
El modo de representacin elegido
debe facilitar la aplicacin de los
operadores genticos
Funcin de aptitud
Es fundamental disear una funcin
de aptitud adecuada para encontrar
las soluciones deseadas
Mximos locales
Se producen cuando ocurre una
convergencia prematura por la dominancia de uno de los individuos
[853]
Ejemplo de aplicacin de un algoritmo gentico: f (x) = x2
En esta seccin se describir un ejemplo sencillo en el que se aplica un AG
para maximizar la funcin f (x) = x2 y donde x es una variable que toma valores
numricos en el intervalo [0,31] [87]. Lgicamente, este problema se puede resolver
de forma analtica y no es necesario el uso de un AG. Sin embargo, el ejemplo ayudar
a comprender con facilidad la forma en la que se aplica un AG.
Dado que x puede tomar valores entre 0 y 31, elegimos un modo de representacin
de cadenas binarias de 5 bits, donde 00000 representa el nmero 0 y 11111 el 31. En la
tabla 28.1 se muestra la poblacin inicial formada por 6 posibles soluciones (escogidas
de forma aleatoria): 21, 8, 18, 19, 30 y 9. En la segunda columna se representan los
cromosomas equivalentes a la representacin binaria de estos nmeros. Finalmente,
en la cuarta columna se muestra el valor de aptitud de cada una de las soluciones,
siendo el nmero 30 la que tiene un valor mayor de aptitud.
ndice
1
2
3
4
5
6
Cromosoma
10101
00100
10010
10011
11110
01001
Entero x
21
8
18
19
30
9
Promedio
Mximo
Aptitud f (x)
441
64
18
361
900
81
361
900
Una vez que el conjunto de soluciones inicial est disponible, debe comenzar
el proceso de seleccin y generar los descendientes. En este ejemplo, se ha elegido
un modo de seleccin por torneo, en el que los cromosomas se enfrentan por pares.
La forma de organizar los enfrentamientos es aleatoria y, al menos, cada cromosoma
debe participar en alguno de los torneos. En este caso, al tener 6 cromosomas, sern
necesarios 6/2 = 3 enfrentamientos. En la tabla 28.2 se puede apreciar como en una
primera ronda, de forma aleatoria, se han enfrentado los pares de cromosomas (2,5) ,
(1,6) y (3,4). Para mantener una poblacin de seis individuos, se ha vuelto a realizar
un segundo torneo (las 3-6) en el que se han enfrentado los pares: (1,5) , (2,3) y (4,6).
Despus de la seleccin se aprecia un incremento notable de la aptitud media en los
miembros de la generacin, aunque se mantiene el valor mximo.
ndice
1
2
3
4
5
6
Torneos
2,5
1,6
3,4
1,5
2,3
4,6
Cromosoma
11110
10101
10011
11110
10010
10011
Entero x
30
21
19
30
18
19
Promedio
Mximo
Aptitud f (x)
900
441
361
900
324
361
547
900
Cuadro 28.2: Seleccin por torneo. Enfrentamiento entre los cromosomas del conjunto inicial
C28
f(x) = x
[854]
Realizada la seleccin se pueden aplicar operadores genticos de cruce y mutacin. En la tabla 28.3 se aplican operaciones de cruce en un slo punto entre los cromosomas 1-2, 3-4 y 5-6. Tal como se puede apreciar los cruces no se realizan siempre
en el mismo punto, en el primer caso se realiza en el cuarto punto, en el segundo punto para los cromosomas 3-4 y en el tercer punto para el par de cromosomas 5-6. Por
otro lado, la mutacin solo se produce en el ltimo bit del cromosoma 4 y como se
puede observar, en este caso, la mutacin es daina ya que disminuye la aptitud de
la solucin. En este ltimo grupo la aptitud promedio disminuye pero nalmente se
encuentra el mximo global y, por tanto, no es necesario continuar.
Caso de estudio: juego de las 8 reinas
El problema de las 8 reinas consiste en ubicar las reinas en un tablero de 8x8 de
tal forma que no se ataquen entre s. Dos reinas no se atacan si no se encuentran en la
misma diagonal. Un AG es apropiado para este problema y puede ayudar fcilmente
a determinar las diferentes combinaciones existentes en la ubicacin de cada reina en
el tablero. Para un tablero de 8x8 y 8 reinas existen 12 patrones o soluciones correctas
posibles. En la tabla 28.4 se muestra uno de ellos.
ndice
1
2
3
4
5
6
Cromo.
1111|0
1010|1
10|011
11|110
100|10
100|11
Punto
4
4
2
2
3
3
Cruce
11111
10100
10110
11011
10011
10010
Muta.
11111
10100
10110
11010
10011
10100
Entero x
31
20
22
26
19
18
Promedio
Mximo
Aptitud f
961
400
484
676
361
324
534
961
Q
Q
Q
Q
Q
Q
Q
Q
Cuadro 28.4: Una de las soluciones al problema de las 8 reinas
[855]
La funcin de adaptacin es la que nos debe indicar la bondad de una solucin.
En este caso una solucin perfecta es aquella en la que no existe conictos entre las
reinas. En dicha funcin se podran contabilizar los conictos, es decir, cuantas reinas
se encuentran en la misma diagonal. Por tanto, cuando menor sea el valor devuelto por
esta funcin, mejor ser la solucin.
El procedimiento que se puede seguir para resolver el problema es el siguiente:
Generacin de una poblacin inicial
Evaluar la poblacin. Obtener el valor de adaptacin para cada una de las
soluciones de la poblacin.
Ventajas
Dos de las grandes ventajas que
proporcionan las redes neuronales
son: procesamiento en paralelo y
conocimiento distribuido
Salida
+1
Entrada
-1
Entrada
Entrada
28.2.3.
Redes neuronales
Las redes neuronales son un paradigma de aprendizaje y procesamiento automtico. Se basa en el modo de procesamiento del cerebro humano en el que existe multitud
de neuronas interconectadas entre s y colaboran para producir estmulos de salida. La
neurona esta formada por el cuerpo o soma, en donde se encuentra el ncleo. Del
cuerpo de la clula surgen diversas ramicaciones conocidas como dendritas y sale
una bra an ms larga denominada axn. La conexin entre neuronas se realiza a
travs del axn y las dendritas; a esta unin se le conoce como sinapsis. Las neuronas
emiten seales y stas se propagan a lo largo de la red gracias a reacciones electroqumicas. Las sinapsis liberan sustancias qumicas transmisoras y entran en las dendritas,
de tal forma que se eleva o se reduce el potencial elctrico de la clula. Cada clula
tiene un nivel o valor umbral de activacin; cuando este se rebasa se enva al axn un
impulso elctrico que llega al resto de neuronas. Las sinapsis que aumentan el potencial son conocidas como excitadoras mientras que las que producen el efecto inverso
son conocidas como inhibidoras.
Una red neuronal articial est constituida por una serie de nodos conectados entre s. A cada una de las conexiones se le asigna un peso numrico wi que determina
la inuencia de dicha conexin en la propia neurona. Las redes neuronales articiales
representan mecanismos potentes para el procesamiento paralelo y distribuido; paralelo porque en la red existen varias neuronas al mismo tiempo procesando informacin
y distribuido porque el conocimiento est distribuido a lo largo de toda la red y no
focalizado en un punto concreto. En la fura 28.41 se muestra el esquema general de
una unidad mnima de procesamiento en una red neuronal.
Tal como se puede apreciar, la neurona recibe una o varias entradas. En cada una de
las conexiones existe asociado un peso wi que determina el grado de inuencia de la
entrada conectada a la neurona. En la propia neurona, existe una funcin base h que se
encarga de combinar los valores de entrada teniendo en cuenta los pesos. Normalmente
la funcin base es una suma ponderada j wj entradaj . Una vez combinados los
valores de entrada se enva el valor calculado a la funcin de activacin g. Dicha
funcin determinar el valor de salida de la neurona. Existen multitud de funciones
de activacin, las ms frecuentes son: funcin escaln (gura 28.38), funcin signo
(gura 28.39) y funcin sigmoidea (gura 28.40).
C28
Figura 28.36: En este caso la funcin de adaptacin devolvera el valor 1 debido al conicto entre la reina situada en la esquina superior izquierda, y la reina situada en la esquina inferior derecha. La solucin
no sera ptima
[856]
Sinapsis
Axn de otra clula
Dendrita
Axn
Ncleo
Entrada 1
Entrada 2
...
w1
w2
w3
ai
Salida
Entrada n
Figura 28.41: Unidad mnima de procesamiento en una red neuronal.
1
0
si x t
si x < t
1
-1
si x 0
si x < 0
1
1+ex
[857]
Las unidades neuronales tambin pueden funcionar como puertas lgicas [79], tal
como se muestra en la gura 28.42. De esta forma, es posible construir redes que
calculen cualquier funcin booleana de las entradas.
W=1
W=1
W=-1
t = 1.5
W=1
t = 0.5
t = -0.5
W=1
Puerta AND
Puerta OR
Puerta NOT
Figura 28.42: Unidades con funcin escaln que actan como puertas lgicas.
Por ltimo, cabe mencionar una de las principales caractersticas de las redes
neuronales: su alto nivel de adptabilidad. Mediante procesos de aprendizaje es posible
ajustar los pesos de las conexiones y adaptarse correctamente a cualquier situacin.
Dicho aprendizaje puede ser supervisado o automtico. En el caso del aprendizaje
supervisado se conocen las entradas de la red y como deberan ser las salidas para cada
una de esas entradas. El ajuste de los pesos se realiza a partir del estudio de la salida
real obtenida a partir de las entradas y de la salida esperada, intentando minimizar el
error en la medida de los posible. Por otro lado, el aprendizaje automtico dispone
nicamente de los valores de entrada. Este tipo de algoritmos trata de ajustar los pesos
hasta encontrar una estructura, conguracin o patrn en los los datos de entrada.
Aplicaciones de las redes neuronales
Las redes neuronales se emplean principalmente para el aprendizaje de patrones
y clasicacin; en algunas ocasiones pueden ser una buena alternativa a sistemas
complejos de reglas y mquinas de estados. A continuacin se detalla una lista de
aplicaciones generales y algunas de las aplicaciones en el desarrollo de videojuegos:
1. Aplicaciones generales
C28
Reconocimiento facial
Anlisis de imagen
Modelos nancieros
Perles de mercado-cliente
Aplicaciones mdicas (sntomas y diagnstico de enfermedades)
Optimizacin de procesos industriales
Control de calidad
Deteccin de SPAM en correo electrnico
Reconocimiento de voz
Reconocimiento de smbolos en escritura
[858]
Estructuras de red
Existen multitud de tipos de estructuras de red y cada una de ellas otorga diferentes
caractersticas de cmputo [79]. Todos ellos se pueden clasicar en dos grandes
grupos: redes de alimentacin progresiva y redes recurrentes. En el primer caso,
las conexiones son unidireccionales y no se forman ciclos. Las neuronas de un nivel
o capa slo pueden estar conectadas con las del nivel siguiente. En la gura 28.43 se
muestra un ejemplo de una red de alimentacin progresiva con dos niveles; dicha red
consta de dos entradas, dos neuronas en la capa oculta y una nica salida. Al no existir
ciclos en las conexiones, el clculo se realiza de forma uniforme desde las unidades
de entrada hasta las unidades de salida.
W13
Entrada 1
W14
W23
Entrada 2
H3
H4
W24
W35
Salida
O5
W45
A la hora de disear una red neuronal hay que resolver una serie de cuestiones
importantes:
Nmero de entradas
Nmero de salidas
Nmero de niveles
Cantidad de neuronas en cada una de las capas o niveles intermedios.
Cantidad de ejemplos de entrada para entrenar la red
Precisin de la red
N de Neuronas
N de Neuronas
Figura 28.43: Red de alimentacin progresiva de dos niveles: dos entradas, dos neuronas en la capa oculta
y un nodo de salida
[859]
A las dos primeras cuestiones es fcil encontrar una respuesta. Para la mayora
de problemas conocemos el nmero de entradas y las salidas deseadas. En el caso de
encontrar ciertas dicultades a la hora de elegir el nmero de parmetros de entrada
existe una relacin directa con los patrones que pretendemos aprender en la red.
Parmetros = log2 P
donde P es el nmero de patrones. Por otro lado, si queremos calcular un nmero de
patrones estimado, se puede emplear la siguiente frmula:
P = W 1/
j=0 (tj
aj ) 2
C28
Bsqueda de Equilibrio
[860]
X0
X1
X2
-W0
1
W1
Salida
W2
X1
0
0
1
1
X2
0
1
0
1
Salida
0
0
0
1
[861]
1. Si la salida es la esperada en funcin de las entradas, no se realizan ajustes en
los pesos.
2. Si la salida es 1, pero debera ser 0, se reducen los pesos de las conexiones
activas de acuerdo a un valor constante.
3. Si la salida es 0, pero debera ser 1, se aumentan los pesos de las conexiones
activas de acuerdo a la misma constante.
Para entrenar la red se toman de forma aleatoria los casos expuestos anteriormente
y se introducen en las entradas. Supongamos que inicialmente todos los pesos valen
cero (tal como se coment anteriormente, el valor inicial de los pesos debe ser bajo).
Supongamos tambin, que el valor de incremento/decremento empleado para los pesos
es 1.
Si el caso 1: X1 = 0, X2 = 0 fuera el primero en utilizarse para entrenar la red,
X0 W0 + X1 W1 + X2 W2 = 0, debido a que todos los pesos valen cero. Por tanto,
la neurona de salida no se activa. En realidad es el valor esperado y, por tanto, no se
producen modicacin de los pesos. En realidad, el nico ejemplo o caso que puede
producir un error con todos los pesos a cero, es el caso 4.
Supongamos que el caso 4 es el siguiente ejemplo empleado para entrenar la red.
X1 = 1, X2 = 1 y la salida esperada es 1; sin embargo X0 W0 + X1 W1 + X2 W2 = 0.
Se esperaba una activacin de la salida que no se ha producido y, as, es necesario
modicar los pesos incrementando los pesos de las neuronas activadas (en este caso
todas las entradas):
W0 = W0 + C = 0 + 1 = 1
W1 = W1 + C = 0 + 1 = 1
W2 = W2 + C = 0 + 1 = 1
De forma iterativa se van aplicando los casos de entrada hasta que no se producen
errores en la salida. Finalmente, para este ejemplo, el ajuste apropiado de los pesos es
el siguiente: W0 = 2, W1 = 1, W2 = 2.
Aprendizaje supervisado mediante retropropagacin (backpropagation)
Cuando tenemos ms de un nivel en la red (ver gura 28.47), el mtodo anterior
resulta insuciente y es necesario utilizar otros como el de retropropagacin. Una de
las condiciones indispensables para poder aplicar este mtodo, es que las funciones de
activacin empleadas en las neuronas deben ser derivables. Bsicamente se calcula el
error producido en la salida y lo propaga hacia las capas intermedias para ajustar los
pesos. El algoritmo consta de los siguientes pasos:
1. Denir una conguracin inicial de la red.
2. Inicializar los pesos con valores aleatorios pequeos
Elegir un patrn y ubicarlo en la entrada de la red
Simular el funcionamiento de la red y observar la activacin de neuronas
en la capa oculta y la salida
Calcular el error producido en la salida
C28
3. REPETIR
[862]
X0
H1
X1
O1
H2
X2
X3
H3
X4
O2
Calcular las derivadas parciales del error con respecto a los pesos de salida
(unin de la capa oculta y la de salida)
Calcular las derivadas parciales del error con respecto a los pesos iniciales
(unin de las entradas con la capa oculta)
Ajustar los pesos de cada neurona para reducir el error.
4. FIN REPETIR
5. Continuar hasta minimizar el error.
En el caso de tener un solo nivel, la actualizacin de los pesos en funcin del error
es sencilla:
Error = valor esperado (T ) - valor real obtenido (O)
Si el error es positivo, el valor esperado es mayor que el real obtenido y por tanto ser
necesario aumentar O para reducir el error. En cambio, si el error es negativo habr
que realizar la accin contraria: reducir O.
Por otro lado, cada valor de entrada contribuir en la entrada de la neurona segn
el peso: Wj Ij (peso W y valor de entrada I). De esta forma, si Ij es positivo, un
aumento del peso contribuir a aumentar la salida y, si por el contrario, la entrada Ij
es negativa, un aumento del peso asociado reducir el valor de salida O. El efecto
deseado se resume en la siguiente regla:
Wj = Wj + Ij Error
[863]
En el caso de tener varios niveles, la actualizacin de pesos es un poco ms
compleja (ver gura 28.48). Los pesos se actualizan a partir del error cometido, dicho
error se propaga desde las primeras capas hasta las nales. La idea general es evaluar
las consecuencias del error y dividirlas entre todos aquellos que hayan contribuido
a dicho error. En primer lugar se actualizan los pesos de las conexiones entre las
neuronas de la capa oculta y la de salida. Las salidas aj de las neuronas de la capa
oculta supondrn la entrada de las neuronas de salida.
Salida
Oi
Wj,i
dad
aj
cultas
Entrada
Wj,i = Wj,i + aj i
Wj,i i
C28
j = g (entradaj )
[864]
Wk,j = Wk,j + Ik j
Ejercicio propuesto: Dada una red neuronal con una nica neurona oculta,
con patrones de entrada P1 (1 0 1), P2 (1 1 0), P3 (1 0 0), llevar acabo un
proceso de entrenamiento mediante backpropagation para ajustar los pesos
de la red y que sta se comporte como la funcin XOR
28.3.
Algoritmos de bsqueda
28.3.1.
Problemas y soluciones.
[865]
La actividad desarrollada por cada agente, viene dada por la secuencia de
transiciones o acciones realizadas hasta dicho momento. Pongamos por ejemplo un
problema lgico clsico como es siguiente:
Sea una par de islas llamadas IslaA e IslaB. En IslaA hay una oveja, una col, un
lobo, una barca y un barquero. La cuestin a resolver es cmo llevar al lobo, la oveja
y la col a IslaB sin perder a nadie en el intento. Si eres el barquero has de considerar
que: slo puedes trasladar un objeto o animal en cada momento en la barca, que la
oveja no se puede quedar sola con la col, ya que se la comera, y que igual destino
correra la oveja a solas con el lobo.
<O
>B <B
LC#BO
LOC#B
<B >B
LCB#O
<C
>C
L#BOC
>O <O
LOB#C
<L >L
<L
>L
O#BLC
>B
<B
OB#LC
<O >O
#BLOC
>C <C
OCB#L
<O >O
C#BLO
<B >B
B#LOC
En un primer momento para trabajar el problema propuesto denimos los elementos que van a congurar un estado, as como las acciones que podramos hacer en cada
uno de ellos. La forma de describir una foto instantnea de una situacin es mediante
la enumeracin de los elementos que hay en cada isla. Llamemos al lobo L, a la oveja
O, a la col C y al barquero junto con la barca B, y determinemos que IslaA est a la
izquierda y IslaB est a la derecha. De esta manera representarmos cualquier situacin
del problema mediante una cadena de caracteres del tipo LOCB# que reeja la situacin inicial donde el lobo, oveja, la col y el barquero estn en IslaA, o por ejemplo
esta otra situacin LC#OB donde el barquero y la oveja se encuentran en IslaB y el
lobo y la col en IslaA.
Una vez jados los estados de nuestro problema de la isla, tendremos que enumerar
las acciones que se pueden desarrollar en un determinado estado. Pongamos por
ejemplo el estado LOCB#, en esta situacin las acciones que podemos realizar son:
coger la barca e ir de vacio a IslaB, que representamos mediante la secuencia de
caracteres >B, llevar el lobo a IslaB >L, llevar la oveja >O o llevar la col >C.
Considerando solamente las acciones vlidas, que no provocan la prdida de algn
elemento, tendremos que en el estado LOCB# slo son correctas las acciones: >B y
>O.
Para denir este conjunto de acciones establemos la funcin sucesor de un estado
como el conjunto de pares (accin,estado) como los estados que son accesibles
desde el mismo mediante una accinn concreta. Por ejemplo sucesor(LOCB#)
= ((>B,LOC#B),(>O,LC#BC)). En estos momentos hemos denido el espacio de
estados del problema (28.50).
C28
LOCB#
>O
[866]
El chero problema.py contiene, en lenguaje python, una realizacin de estos conceptos mediante la especicacin de una clase abstracta Problema, que representa
cualquier problema en terminos de espacio de estados, y una clase derivada ProblemaIsla para el problema de la isla.
Listado 28.8: class Problema
1 class Problema:
2
def __init__(self,estadoInit,nombre="NoDef"):
3
self.id=nombre
4
self.estInit=estadoInit
5
6
def estadoValido(self,estado):
7
pass
8
9
def sucesor(self,estado):
10
pass
11
12
def meta(self,estado):
13
pass
14
15
def estadoInit(self):
16
return self.estInit
")]
self.g["OCB#L"]=[(">C","O#BLC"),(">O","C#BLO")]
self.g["OB#LC"]=[(">B","O#BLC"),(">O","#BLOC")]
self.g["C#BLO"]=[("<L","LCB#O"),("<O","OCB#L")]
self.g["#BLOC"]=[("<O","OB#LC"),("<B","B#LOC")]
self.g["B#LOC"]=[(">B","#BLOC")]
LOCB#
>O
LC#BO
<B
LCB#O
>C
L#BOC
<O
LOB#C
>L
O#BLC
<B
OB#LC
>O
#BLOC
def estadoValido(self,estado):
return estado in self.g.keys()
def sucesor(self,estado):
if self.estadoValido(estado):
return self.g[estado]
[867]
else:
return None
33
34
35
36
37
def meta(self,estado):
return estado=="#BLOC"
28.3.2.
ID0*
LOCB#
ID0
LOCB#
>B
ID1*
ID2*
LC#BO
>O
ID1*
ID2
LOC#B
LC#BO
<B
<O
ID3*
ID4*
LCB#O
LOCB#
ID0
Inf
LC#BO
None
LOCB#
ID1
>O
Inf1
ID0
None
Inf0
C28
>B
Accion
ID0
LOCB#
Padre
Una estructura de datos, que permite realizar este propsito es el rbol. Un rbol
puede incorporar la informacin relativa a un estado en sus nodos, as como las
acciones realizadas, y etiquetar los enlaces entre un padre y sus hijos. Un rbol cuyo
nodo raz represente el estado inicial de un problema y la relacin lial entre nodos
hijos y padres venga explicitada por el conjunto de acciones posibles segn el espacio
de estados se denomina rbol de bsqueda, y cada una de sus ramas representa un
camino posible.
>O
LOC#B
Estado
[868]
.estado,self.inf)
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def camino(self):
"""Retorna la lista de nodos hasta el nodo raiz."""
p=[]
n=self
while n<>None:
p.insert(0,n)
n=n.padre
return p
def GeneraNodoHijo(self,ac,est):
n=NodoArbol(self,ac,est,{Prof:n.inf[Prof]+1})
n.val=n.inf[Prof]
return n
El algoritmo descrito selecciona, del conjunto de nodos hoja del rbol de bsqueda, un nodo para analizar. Primero verica que no es una solucin y posteriormente lo
analiza. Este anlisis consiste en obtener a partir del problema, con la funcin sucesor,
el conjunto de nuevos nodos hijos e introducirlos en el rbol de bsqueda como nuevas
hojas al mismo tiempo que el nodo estudiado deja de ser hoja.
De lo descrito anteriormente el proceso de seleccin del nodo hoja a expandir es
de una importancia , ya que de una buena o mala seleccin depender que la bsqueda
de un nodo meta sea ms o menos eciente.
[869]
Al mecanismo seleccionado para la eleccin del nodo hoja que hay que
expandir se denomina estrategia de bsqueda.
[>O,1]
9
OB#LC
[<B,1]
8
O#BLC
[>L,1]
6
LOB#C
[<O,1]
4
OCB#L
[<O,1]
L#BOC
."""
16
pass
17
18 class PilaNodo(AlmacenNodos):
19
"""Modela un Almacen como una Pila: Ultimo en entrar, primero
C#BLO
[<C,1]
3
[>L,1]
en salir."""
LCB#O
[<B,1]
1
LC#BO
LOC#B
[>O,1] [>B,1]
0
LOCB#
20
def __init__(self,nombre):
21
AlmacenNodos.__init__(self,nombre)
22
self.p=[]
23
self.lest=[]
24
def esVacia(self):
25
return len(self.p)==0
26
def add(self,elem):
27
self.p.append(elem)
28
self.lest.append(elem.estado)
29
def get(self):
30
return self.p.pop()
31
def esta(self,estado):
32
return estado in self.lest
33
34 class ColaNodo(AlmacenNodos):
35
""" Modela un Almacen como una Cola: Primero en entrar, primero
36
37
38
39
40
41
en salir."""
def __init__(self,nombre):
AlmacenNodos.__init__(self,nombre)
self.c=[]
self.lest=[]
def esVacia(self):
return len(self.c)==0
C28
10
#BLOC
[870]
42
43
44
45
46
47
48
#BLOC
[>O,1]
8
LOB#C
OB#LC
[<L,1]
7
[<B,1]
O#BLC
[>C,1]
6
OCB#L
[<O,1]
4
L#BOC
C#BLO
[<C,1]
3
[>L,1]
LCB#O
[<B,1]
1
LC#BO
LOC#B
[>O,1] [>B,1]
0
LOCB#
[871]
28.3.3.
Ahora ya tenemos un valor para decidir que nodo hoja coger, el menor valor de
costo de los nodos hoja, o que el conjunto de nodos hojas estn ordenados de forma
creciente por el valor del costo.
C(n)
(Acc,val)
n1
C(n1)=C(n)+val
C28
Inicial
(28.3)
[872]
14
15
16
def get(self):
return heapq.pop(self.p)
A la estrategia que selecciona un nodo hoja con valor de costo menor se denomina
estrategia de costo uniforme.
Como ejemplo denimos un nuevo problema. El problema es el conocido como 8
puzzle donde se tiene un artilugio con 8 piezas numeradas del 1 al 8 y un espacio sin
pieza. Las piezas se puden deslizar al espacio en blanco y as cambiar la conguracin
del puzzle. Si se representa una conguracin concreta del 8 puzzle con una cadena
de 9 caracteres donde los 3 primeros representan la primera la, y as sucesivamente
y el espacio en blanco por el caracter B, y consideramos que los movimientos son los
del espacio blanco. Si consideramos que de los cuatro movimientos posibles el costo
de los movimientos hacia arriba e izquierda van a costar 2 unidades frente a la unidad
de los movimientos abajo y derecha.
El problema propuesto es denido en el siguiente cdigo ejemplo.
Listado 28.15: Problema del 8 puzzle.
1 class Problema8Puzzle(Problema):
2
3
def __init__(self,estadoI,estadoFin,h=DESCOLOCADAS):
4
Problema.__init__(self,estadoI,"8Puzzle")
5
self.estadoFin=estadoFin
6
self.h=h
7
8
def estadoValido(self,estado):
9
valido=True
10
for ficha in range(1,9):
11
valido=valido and (str(ficha) in estado)
12
valido=valido and (len(estado)==9)
13
valido=B in estado
14
return valido
15
16
def sucesor(self,estado):
17
if self.estadoValido(estado):
18
PB=estado.find(B)
19
FB=PB/3
20
CB=PB %3
21
mv=[]
22
if FB<2:
23
NP=(FB+1)*3+CB
24
est=list(estado[:])
25
est[PB]=est[NP]
26
est[NP]=B
27
mv.append(({Acc:AB,val:1},"".join(est)))
28
if FB>0:
29
NP=(FB-1)*3+CB
30
est=list(estado[:])
31
est[PB]=est[NP]
32
est[NP]=B
33
mv.append(({Acc:AR,val:2},"".join(est)))
34
if CB<2:
35
NP=FB*3+CB+1
36
est=list(estado[:])
37
est[PB]=est[NP]
38
est[NP]=B
39
mv.append(({Acc:DE,val:1},"".join(est)))
40
if CB>0:
41
NP=FB*3+CB-1
42
est=list(estado[:])
43
est[PB]=est[NP]
44
est[NP]=B
45
mv.append(({Acc:IZ,val:2},"".join(est)))
46
return mv
[873]
47
48
49
50
51
else:
return None
def meta(self,estado):
return estado==self.estadoFin
C28
[874]
Este valor del costo que nos queda por realizar desde un estado para alcanzar un
estado meta se denomina valor de la heurstica.
Para aadir esta heurstica modicaremos la denicin de un problema incorporando una nueva funcin que evalue el valor de la heurstica. Dado un estado n la
funcin heuristica h(n) ser el valor estimado del costo que queda por realizar hasta
un estado meta.
Listado 28.16: Problema Heurstica
1 class Problema:
2
def __init__(self,estadoInit,nombre="NoDef"):
3
self.id=nombre
4
self.estInit=estadoInit
5
6
def estadoValido(self,estado):
7
pass
8
9
def sucesor(self,estado):
10
pass
11
12
def meta(self,estado):
13
pass
14
15
def estadoInit(self):
16
return self.estInit
17
18
def heuristica(self,estado):
19
pass
Inicial
C(n)
n
H(n)
Meta
[875]
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
n=0
for i in range(len(estado1)):
if estado1[i]<>estado2[i]:
n=n+1
return n
def movDistintas(self,estado1,estado2):
n=0
for i in range(len(estado1)):
if estado1[i]<>estado2[i]:
py1=i/3
px1=i %3
pc=estado2.find(estado1[i])
py2=pc/3
px2=pc %3
n=abs(px2-px1)+(py2-py1)
return n
def setHeuristica(self,h=DESCOLOCADAS):
self.h=h
def heuristica(self,estado):
if self.h==DESCOLOCADAS:
return self.posDistintas(estado,self.estadoFin)
elif self.h==CUNIFORME:
return 0
else:
return self.movDistintas(estado,self.estadoFin)
Llegados a este punto, y para combinar la informacin del costo desarrollado con
la del costo por desarrollar o funcin heurstica, deniremos el valor del nodo como
la suma del costo ms el valor de la funcin heurstica.
f (n) = C(n) + H(n)
(28.4)
1 def GeneraNodo(p,n,ac,est,estr=A):
2
MUCHO=10000000
3
prof=n.inf[Prof]+1
4
coste=n.inf[Coste]+ac[val]
5
h=p.heuristica(est)
6
7
if estr==A:
val=coste+h
8
elif estr==PROF: val=MUCHO-prof
9
elif estr==ANCH: val=prof
10
elif estr==COST: val=coste
11
else:
val=h
C28
[876]
12
13
14 def BuscaSolucionesA(prob,estr=A):
15
frontera=AlmacenOrdenado()
16
h=prob.heuristica(prob.estadoInit())
17
n=NodoArbol(None,None,prob.estadoInit(),{Prof:0,Coste:0,h
18
19
20
21
22
23
24
25
26
27
28
29
:h},0)
solucion=None
frontera.add((n.val,n.estado,n))
while (not frontera.esVacia()) and (solucion==None):
n=frontera.get()
if prob.meta(n.estado):
solucion=n
else:
for ac,est in prob.sucesor(n.estado):
if not frontera.esta(est):
h=GeneraNodo(prob,n,ac,est,estr)
frontera.add((h.val,h.estado,h))
return solucion
28.4.
Num de Nodos
>30000
16018
14873
2326
S. ptima
No
No
Si
Si
Planicacin de caminos
28.4.1.
Puntos visibles
[877]
El movimiento de un actor se desarrolla como el desplazamiento entre puntos
visibles. Este movimiento puede generalizarse como el desplazamiento por los nodos
de un grafo, y de esta forma denimos el problema de la construccin de un camino
desde un punto visible a otro punto visible como un problema de encontrar el camino
entre dos nodos de un grafo.
28.4.2.
Las operaciones bsicas denidas son: aadir un arco,un nodo y obtener los
sucesores de un nodo. Segn sea el acceso de un nodo a otro, encontraremos nodos
donde solo es posible en una direccin que denominaremos grafos dirigidos; en caso
contrario, tendremos grafos no dirigidos. Esta diferencia la introducimos en la clase
grafo mediante el valor tipo de la clase, que por defecto ser dirigido.
Denido la estructura de grafo, podemos construir un problema como se coment
en 28.3.1 para lo que denimos la clase ProblemaGrafo.
1 class ProblemaGrafo(Problema):
2
def __init__(self,Grafo,NodoInicial,NodoFinal):
3
Problema.__init__(self,NodoInicial,"Grafo")
4
self.estadoFin=NodoFinal
5
self.graf=Grafo
6
def estadoValido(self,Nodo):
7
return Nodo in self.graf.g.keys()
8
def sucesor(self,Nodo):
9
return self.graf.nodosSucesores(Nodo)
10
def meta(self,Nodo):
11
return Nodo==self.estadoFin
12
def estadoInit(self):
13
return self.estInit
C28
[878]
(x x )2 (y y )2
(28.5)
(28.6)
[879]
Listado 28.21: class Grafo con informacin
1 import map
2
3 class GrafoMapa(Grafo):
4
def __init__(self,map,h=EUCLIDEA):
5
self.dato=map
6
self.id="Mapa"
7
self.h=h
8
def addNodo(self,Nodo):
9
pass
10
def addArco(self,NodoOrigen,NodoDestinod,InfArco):
11
pass
12
def nodosSucesores(self,Nodo):
13
f,c=Nodo
14
mv=[]
15
anchura,altura=self.dato.tablero
16
if (f<(altura-1)) and (not self.dato.mapa[f+1][c]==1):
17
mv.append(({Acc:AB,val:1},(f+1,c)))
18
if (f>0) and (not self.dato.mapa[f-1][c]==1):
19
mv.append(({Acc:AR,val:1},(f-1,c)))
20
if (c<(anchura-1)) and (not self.dato.mapa[f][c+1]==1):
21
mv.append(({Acc:DE,val:1},(f,c+1)))
22
if (c>0) and (not self.dato.mapa[f][c-1]==1):
23
mv.append(({Acc:IZ,val:1},(f,c-1)))
24
return mv
25
26
def heuristica(self,nodo1,nodo2):
27
y1,x1=nodo1
28
y2,x2=nodo2
29
s=0.0
30
if self.h==EUCLIDEA:
31
s= ((x1-x2)**2.0+(y1-y2)**2.0)**(1.0/2.0)
32
elif self.h==CUNIFORME:
33
s=0.0
34
else:
35
s= abs(x2-x1)+abs(y2-y1)
36
print "H( %s, %s)= %d" %(str(nodo1),str(nodo2),s)
37
return s
28.4.3.
La primera opcin para la obtencin del grafo de los puntos visibles es de forma
manual. Dado un mapa se sealan en l los puntos y se unen para generar el grafo.
Esto obliga a construir un editor de grafos de puntos visibles y adjuntar a cada mapa
su grafo correspondiente.
C28
Una alternativa a la construccin manual es construir un grafo de forma automtica. El primer mtodo parte de un estructura particular de un mapa. Partamos de un
mapa construido con guras geomtricas denidas por unos vertices y unas arstas.
Consideremos las guras que conguran los obstculos o paredes de nuestro mapa,
y extendamos su geometra una distancia igual al tamao de nuestro agente (gura
28.64.
[880]
28.4.4.
Aumentando la resolucin
Con los GPV (grafos de puntos visibles), obtenidos se tiene un conjunto no muy
numeroso de putos visibles y con una distribucin no muy uniforme por todo el mapa.
Para aumentar el nivel de realismo en el movimiento de los agentes se tendra que
aumentar el nmero de puntos visibles, y que estos estn distribuidos por el mapa
de una forma uniforme. En un principio si consideramos una distancia mnima dm
para separar puntos visibles, tendramos una forma de ajustar el realismo o nuestra
resolucin mediante el ajuste del valor de dm.
Si adems consideramos que los movimientos que poden realizar los agentes estn
acotados, tendremos un nuevo mecanismo de generar un grafo de puntos visibles. Este
nuevo mecanismo es el siguiente:
1. Colocar un punto visible en una posicin del mapa.
2. Aadir este punto visible a una lista de PV no visitados.
celda[1]
self.s = pygame.display.set_mode(self.size)
loseta=self.CreaCelda((113,132,132))
muro=self.CreaCelda((255,181,66))
inicio=self.CreaCelda((255,0,0))
fin=self.CreaCelda((0,0,255))
self.elementos=[loseta,muro,inicio,fin]
[881]
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
if m==None:
self.CreaMap(self.tablero[0],self.tablero[1])
else:
self.mapa=m
def CreaCelda(self,color):
s=pygame.Surface((self.celda[0],self.celda[1]),0,16)
s.fill(color)
pygame.draw.rect(s,(255,181,66),pygame.Rect(0,0,20,20),2)
return s
def AnadeCelda(self,color):
self.elementos.append(self.CreaCelda(color))
def CreaMap(self,w,h):
self.mapa=[]
for y in range(h):
self.mapa.append([])
for x in range(w):
self.mapa[y].append(0)
def
PintaMapa(self):
for y in range(self.tablero[1]):
for x in range(self.tablero[0]):
rect=pygame.Rect(x*self.celda[0],y*self.celda[1],
self.celda[0],self.celda[1])
self.s.blit(self.elementos[self.mapa[y][x]],rect)
def ModificaMapa(self,celdaIndx,infor):
titulo=pygame.display.get_caption()
pygame.display.set_caption(infor)
NoFin=True
while NoFin:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key ==
pygame.K_ESCAPE:
NoFin=False
pos=pygame.mouse.get_pos()
pos=pos[0]/self.celda[0],pos[1]/self.celda[1]
b1,b2,b3= pygame.mouse.get_pressed()
rect=pygame.Rect(pos[0]*self.celda[0],pos[1]*self.celda
[1],self.celda[0],self.celda[1])
if b1:
self.s.blit(self.elementos[celdaIndx],rect)
self.mapa[pos[1]][pos[0]]=celdaIndx
elif b3:
self.s.blit(self.elementos[0],rect)
self.mapa[pos[1]][pos[0]]=0
pygame.display.flip()
pygame.display.set_caption(titulo[0])
def SalvaMapa(self,fn):
f=csv.writer(open(fn,"wr"),delimiter=,)
for l in self.mapa:
f.writerow(l)
def CargaMapa(self,fn):
f=csv.reader(open(fn,rb),delimiter=,)
self.mapa=[]
for r in f:
self.mapa.append([int(x) for x in r])
def DefMuros(self):
self.ModificaMapa(1,Definir Muros)
def Entrada(self):
self.ModificaMapa(2,Definir Entrada)
def Salida(self):
self.ModificaMapa(3,Defir Salida)
C28
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
[882]
[883]
28.5.
En esta seccin se mostrar como disear agentes que puedan ser empleados en
videojuegos, siendo esta ltima restriccin importante ya que no se va a pretender
crear agentes que emulen la inteligencia humana, sino que el objetivo ser crear
agentes que hagan que el usuario crea que el videojuego tiene inteligencia, aunque
no sea as. Se presentarn dos enfoques distintos de diseo de agentes uno basado en
comportamientos y otro basado en objetivos, prestando mayor atencin al primero
por ser el ms usual. Para ello primero se dar una denicin del concepto de agente,
y una posible clasicacin de los mismos. Despus se presentarn los tipos de agente
que se suelen construir en los videojuegos. Se nalizar la seccin con una discusin
sobre las implicaciones que tendra construir sistemas multiagentes completos y unas
breves reexiones sobre hacia donde puede tender el uso de agentes y sistemas
multiagentes en el campo de los videojuegos y los retos que hay abiertos.
28.5.1.
Figura 28.70: Desde que el Profesor Nick Jennings enunciara la frase Los agentes constituyen el prximo avance ms signicativo en el
desarrollo de sistemas y pueden ser
considerados como la nueva revolucin en el software, muchos son
los que se han subido al carro indicando que sus aplicaciones son
agentes. Los agentes estn de moda.
Qu es un agente?
C28
En la mayora de las ocasiones, en los videojuegos, los agentes no son desarrollados de manera independiente sino como entidades que constituyen un sistema ms
completo. De este modo, los videojuegos se estn conviertiendo en sistemas multiagente, a partir de ahora se har referencia a ellos como MAS (Multi-Agent Systems).
En estos sistemas existirn un conjunto de agentes que son empleados para simular
personalidades y caracteres diferentes para los personajes de los juegos y que pueden
coordinar sus comportamientos de grupo y colaborar para lograr un n (p.e. matar, o
siendo menos drsticos, hacer prisionero al jugador).
[884]
As, para que un software pudiera ser considerado un agente debera de tener las
caractersticas antes expuestas. No obstante, a lo largo de los aos, los investigadores
del campo han usado nuevas caractersticas para calicar a los agentes, a continuacin
se citan algunas de las recopiladas por Franklin y Graesser [36]:
Continuidad Temporal, con capacidad para ejecutarse ininterrumpidamente.
Racionalidad, con capacidad de elegir siempre la accin correcta en funcin
de lo percibido en el entorno.
Adaptabilidad, con capacidad de cambiar su comportamiento basndose en el
aprendizaje que realice.
Movilidad, con capacidad para trasladarse a travs de una red telemtica.
Otras caractersticas que se asumen como propiedades de los agentes son la
Veracidad, por la que se se supone que un agente no va a proporcionar informacin
falsa a propsito y la Benevolencia, que implica que un agente est dispuesto a ayudar
a otros agentes, si esto no entra en conicto con sus intereses.
Llegados a este punto, cabe entonces hacerse la siguiente pregunta, qu caractersticas debe poseer un sistema para poderle asignar el calicativo de agente? Al
respecto, se debe indicar que no existe un concenso sobre cual es la importancia de
cada una de las propiedades que se han enunciado para denir un agente. Sin embargo, si se puede armar que son la existencia de estas, en mayor o menor medida,
las que distinguen agentes de meros programas informticos (vea gura 28.71). Desde la perspectiva que nos ocupa, el diseo de videojuegos, cabe otra pregunta, qu
caractersticas deben poseer los agentes usados en los videojuegos?
En este sentido a la respuesta antes dada, hay que aadirle un matiz, y este
es el siguiente: Mientras en los agentes del campo de la IA acadmica se pueden
emplear mquinas especiales con gran poder de cmputo para ejecutarse (p.e. la
famosa deep blue), los agentes diseados para juegos tienen su poder de cmputo
limitado a mquinas ms normales y es mucho menor, y adems compartido con otros
procesos intensivos en CPU, como por ejemplo la simulacin fsica, la generacin de
los grcos y el trco de red. Esto conduce al desarrollo de agentes ms limitados
en caractersticas o en el despliegue de las mismas, pero que den sensacin de
inteligencia.
Una de las caractersticas que se han presentado como interesante, y que por otra
parte es resaltada como necesaria en los agentes cuando se le aade el calicativo
de inteligente, es la de racionalidad. Como ya se ha comentado, esta caracterstica
implica que el agente haga siempre lo correcto, pero hay que preguntarse qu es
lo correcto? Una primera aproximacin, y por otra parte usual, es la de denir lo
correcto como aquello que le permite al agente obtener un resultado mejor, por lo tanto
habr que medir el xito. Para ello, es preferible emplear medidas de rendimiento o
utilidad, que estn asociadas al entorno, ms que de desempeo asociadas a cmo
de bien lo hace el agente. Es conveniente resaltar en este punto que no existe una
medida universal para todo agente, cada agente tendr su propia medida y sta ser
dependiente del problema.
Agente y Problema
Los agentes racionales representan
soluciones a problemas.
[885]
Un agente racional deber emprender en cada momento la accin que en principio
maximice su medida de desempeo o rendimiento, basndose en las evidencias
aportadas por la secuencia de percepciones de dicho momento y en el conocimiento
que el agente mantiene de momentos anteriores. De este modo, la racionalidad de un
agente en un instante determinado vendr dada por:
El valor de la medida de desempeo, rendimiento o utilidad del agente.
El conocimiento acumulado por el agente sobre el entorno en el que desempea
su funcin.
El conjunto de acciones que puede realizar en ese instante.
La secuencia de percepciones que ha captado haste ese instante.
En el caso de los videojuegos, un agente racional debera tener una medida
de rendimiento asociado al entorno que permitiera guiar su comportamiento. Por
ejemplo, en los juegos de accin en primera persona (FPS), los agentes podran ser
empleados para simular comportamientos de adversarios y la medida de rendimiento
ser inversamente propocional al nivel de stamina que tiene el jugador tras ejecutar
la accin. La eleccin de la siguiente accin se podra realizar en base a la capacidad
para reducir este valor, o considerando de las que ya ha ejecutado, aquella que mejor
resultado le dio.
28.5.2.
C28
[886]
[887]
Agente Reactivo basado en modelo. En gran cantidad de ocasiones un agente
no podr tomar una decisin teniendo en cuenta una nica percepcin, porque
esta no proporciona toda la informacin necesaria, es por ello necesario emplear
estados que de alguna forma guarden informacin sobre las percepciones
histricas o que ya no son observables. Adems se necesita informacin sobre
cmo evoluciona el mundo, independiente del agente.
En este tipo de agentes la percepcin actual se interpreta a partir del estado anterior utilizando informacin sobre: cmo evoluciona el entorno independientemente del agente y la inuencia en el entorno de las acciones del agente. Los
modelos sern empleados para representar cmo evoluciona el entorno.
Agente Reactivo basado en subsuncin. En este tipo de agentes la toma de
decisin (la accin) se realiza mediante un conjunto de mdulos de comportamiento que realizan tareas. Los comportamientos se agrupan en capas que
estn organizadas en orden decreciente de priodidad. De este modo los comportamientos de las capas inferiores tendrn ms prioridad que las de las capas
superiores. No se pueden ejecutar mdulos de distintas capas, los de las capas
inferiores inhiben a los de las capas superiores. Es decir, cada percepcin puede
acarrear la ejecucin de varios mdulos (al satisfacer las condiciones de ejecucin de los mismos), se deber entonces comprobar que comportamientos no
son inhibidos por otros y llevarlos a cabo.
Agente Deliberativo. Son una mejora sobre los Agentes de Razonamiento
Simblico, y surgen porque con los estados no es suciente para tomar una
decisin, ya que sta muchas veces depende de cual es la misin del agente. Por
tanto se requiere informacin sobre el objetivo o meta del agente. Estos agentes
se caracterizan por la utilizacin de modelos de representacin simblica del
conocimiento (tanto del entorno como de la meta) y suelen basarse en la teora
clsica de planicacin: Parten de un estado inicial y emplean un sistema de
planicacin que genera un conjunto de pasos (un plan) para lograr el objetivo
para el cual fueron diseados. Por tanto, la bsqueda y la planicacin son muy
importantes para la construccin de este tipo de agentes.
La arquitectura deliberativa ms estudiada y extendida es la arquitectura
BDI (Belief, Desire, Intention). Esta se caracteriza por el hecho de que los
agentes que la implementan estn dotados de los estados mentales de Creencias
(modelo del mundo), Deseos (metas) e Intenciones (plan de accin), y para
denir los planes se tienen en cuenta sus creencias y deseos. Su principal
problema a la hora de aplicarse es que requieren un elevado tiempo de respuesta,
esto lo hace especialmente poco tiles en el caso de los videojuegos. No
obstante, recientemente desde el campo acadmico se estn proponiendo formas
de usar la arquitectura BDI para modelar comportamientos de personajes en
videojuegos, por ejemplo algunas pruebas se han realizado en el videojuego
Quake 2.
En ocasiones, las metas por si solas tampoco son sucientes, ya que no basta
con alcanzar una meta sino hacerlo de la mejor forma posible. Para garantizar
la seleccin de la mejor meta es necesario una medida que permita comparar
decisiones o planes. Esta medida se denomina utilidad. Una accin puede tener
ms utilidad que otra, el agente debe ir buscando aquellas que les proporcionen
ms utilidad.
Agente Hbrido. Este tipo de agentes combinan componentes de tipo reactivo
y componentes de tipo deliberativo. Usualmente, la parte reaactiva se encarga
de reaccionar en funcin de lo que ocurra en el entorno sin dedicar demasiado
tiempo a razonar y la parte deliberativa es la que se encarga de planicar a ms
largo plazo y a soportar los procesos de toma de decisin.
C28
[888]
28.5.3.
Los agentes pueden ser programados en base a sus comportamientos. Un comportamiento especica las tareas o servicios que realiza un agente para lograr sus
objetivos. Cada comportamiento puede realizar una tarea simple, que en el caso de
los videojuegos pueden ser, Lanzar una pregunta o Resoplar aunque tambin se
pueden crear comportamientos ms complejos como por ejemplo Huir o Atacar.
Para realizar una programacin basada en comportamientos se deben seguir los
siguientes pasos (vea gura 28.74):
Estudiar el entorno.
Analizar que es lo qu debe ser capaz de hacer el agente. Estudiar cuales son
los comportamientos deseados y cuando ocurren en funcin del entorno.
Asociar a cada comportamiento una funcionalidad. Establecer que es lo que
debe ocurrir como accin asociada a cada comportamiento.
Relacionar los comportamientos. Establecer las conexiones que existen entre
los comportamientos, es decir como se conectan y cuando ocurrren.
El agente reactivo, en alguna de sus variantes, es el ms fcil de construir para
programar agentes basados en comportamientos, ya que realiza tareas sencillas y
de una forma rpida. Es por esto, por lo que resulta ms til en el campo de los
videojuegos. El esquema general del programa de agente reactivo es el que se muestra
en la gura 28.75.
Como se puede observar su funcionamiento se basa en una recepcin de percepciones del entorno (proporcionados por los sensores) que al ser interpretada produce
un cambio en el estado interno del agente, lo cual lleva asociada la seleccin del procedimiento o rutina con la que se debe reaccionar en ese estado a esa percepcion, y
por ltimo se produce la reaccin propiamente dicha. sta consiste en la ejecucin
del procedimiento o rutina. En este esquema de funcionamiento no hay ningn mecanismo explcito de representacin del conocimiento ni hay procesos de manipulacin
sobre el mismo, es esto lo que simplica el diseo y construccin del agente, as como
su uso con tiempos de respuesta inmediatos.
[889]
Este tipo de agente se suele disear de forma parecida a los sistemas basados en
reglas, en los que existe una base de reglas y un motor de inferencia. Cada regla enlaza
una percepcin con una accin y el sistema de inferencia determina que regla ejecutar
en funcin de la percepcin recibida.
Los agentes reactivos ms usados en el campo de los videojuegos son los agentes
reactivos basados en modelos. Para el diseo de este tipo de agentes se emplean
autmatas de estados nitos (en la Seccin 28.1.4 ya se introdujo la idea). Se divide el
comportamiento general del objeto o personaje virtual del videojuego en un nmero
nito de estados, cada uno de ellos corresponde a una situacin del entorno, que tendr
asociado un comportamiento particular del personaje y se establecen las transiciones
que pueden ocurrir entre ellos y las condiciones o reglas que se deben cumplir
para pasar de un estado a otro de una manera determinista, lo cual implica que sea
predecible el comportamiento.
En la gura 28.76 se muestra cual es el esquema de funcionamiento del agente
reactivo basado en un atmata de estados nitos (FSM (Finite State Machine)). El
funcionamiento de este agente se basa en a partir del estado actual, almacenado en
la memoria, comprobar que regla de las posibles reglas que le permiten cambiar de
estado puede ser disparada en funcin de las percepciones del entorno recibidas. El
disparo de esa regla le permite cambiar de estado y ejecutar la accin asociada al
nuevo estado que se alcance. Una vez ejecutada esta accin hay que convertir el estado
alcanzado como estado actual. Conviene notar que solo ser posible disparar una nica
regla y que por lo tanto en cada instante el autmata se encontrar en un nico estado.
La eleccin de los autmatas como arquitectura de funcionamiento se debe a que:
Son simples y rpidos de codicar. Existen muchas formas de programar un
autmata todas ellas razonablemente simples de programar.
Son fciles de depurar. Al estar formados por estados es fcil de depurar ya
que se podra aadir codigo de depuracin en cada uno de ellos.
Tienen poca carga computacional. No poseen procesos computacionales
complejos, su funcionamiento es muy simple.
Son exibles. Pueden ser fcilmente ajustados por el programador para proporcionar el comportamiento requerido por el diseador del juego.
C28
[890]
No es objetivo de esta seccin dar una denicin formal del autmata, para el
propsito de este curso basta con saber que es un modelo de comportamiento de un
sistema o un objeto complejo y que se compone de estados, eventos y acciones. Un
estado representa una situacin. Un evento es un suceso que provoca el cambio de un
estado a otro. Una accin es un tipo de comportamiento.
[891]
a,b
a
a,b,c
c
q1
b,c
q0
q3
q2
a,b,c
Figura 28.77: Autmata que reconoce el lenguaje a(a|b) |(b|c)(a|b|c) , p.e. la cadena abb pertenecera
al lenguaje que reconoce el autmata, sin embargo la cadena acc no pertenecera a dicho lenguaje.
Shadow (Blinky)
Persig iendo
Perseguir();
Si Comecocos ha
Comido una pastilla
o punto especial
Huyendo
Huir();
Si Comecocos lo traga
Figura 28.78: Comportamiento del fantasma Shadow (Blinky) del juego del Comecocos (PacMan),
modelado por medio de una mquina de Moore.
Se emplearn las mquinas de Moore o Mealy para modelar los comportamientos en los videojuegos. Estas mquinas son autmatas de estados nitos
determinista que poseen salida ms all de una decisin sobre si una cadena
pertenece o no al lenguaje que reconoce el autmata.
C28
Inactivo
[892]
[893]
La implementacin de un autmata se puede hacer, de una manera ms eciente
que la expuesta antes, empleando una matriz. La matriz tendr las las etiquetadas
con los estados del autmata y las columnas con los smbolos del alfabeto de entrada.
Cada interseccin [i, j] informa del estado al que pasar el autmata si estando en el
estado i se lee el smbolo j.
Listado 28.23: Autmata de la gura 28.77 (sentencias switch).
#include <stdio.h>
int estado_q0(void) {
char c;
c=getchar();
switch (c) {
case a: return estado_q1();
break;
case b:
case c: return estado_q2();
break;
default: return 0;
}
}
int estado_q1(void) {
int c;
c=getchar();
while ((c==a) || (c==b)) c=getchar();
switch (c) {
case \n: return 1;
break;
case c:
return estado_q3();
break;
default:
return 0;
}
}
int estado_q2(void) {
int c;
c=getchar();
while ((c==a) || (c==b) || (c==c)) c=getchar();
switch (c) {
case \n: return 1;
break;
default:
return 0;
}
}
int estado_q3(void) {
int c;
c=getchar();
while ((c==a) || (c==b) || (c==c)) c=getchar();
switch (c) {
case \n: return 0;
break;
default:
return 0;
}
}
int main (int argc,char *argv[]) {
if(estado_q0())
printf("Es una cadena del Lenguaje\n");
else
printf("\nNo es una cadena del Lenguaje \n");
return 1;
}
C28
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
52
53
54
55
56
57
[894]
estado_siguiente = matriz[estado_actual][entrada]
Esto no es ms que una implementacin de la tabla de transiciones del autmata.
La matriz asociada al autmata de la gura 28.77, es la que se muestra en la gura
28.79. El cdigo en lenguaje C, que corresponde a la implementacin del autmata de
la gura 28.77 de esta forma, es el que se muestra a continuacin.
Listado 28.24: Autmata de la gura 28.77 (tabla transicin).
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
#include <stdio.h>
/* Se define la Tabla de Transicion del automata */
#define N 4 //numero de filas de la tabla de transicion
#define M 3 //numero de columnas de la tabla de transicion
int TablaTrans[N][M] = { 1,2,2,
1,1,3,
2,2,2,
3,3,3};
int AFD() {
int estado_actual, estado_siguiente;
int c;
estado_actual=0;
c=getchar();
while (c!=\n) {
switch (c) {
case a: estado_siguiente=TablaTrans[estado_actual][0];
break;
case b: estado_siguiente=TablaTrans[estado_actual][1];
break;
case c: estado_siguiente=TablaTrans[estado_actual][2];
break;
default: return 0;
}
if (c!=\n) estado_actual=estado_siguiente;
c=getchar();
}
if ((estado_actual==1) || (estado_actual==2)) return 1;
else return 0;
#include <iostream>
#include <string>
using namespace std;
enum StateType {q0, q1, q2, q3, error};
q0
q1
q2
q3
a
q1
q1
q2
q3
b
q2
q1
q2
q3
c
q2
q3
q2
q3
[895]
7
8 int main() {
9
string palabra;
10
int n, i, mensaje=0;
11
StateType CurrentState;
12
13
cout << "Introduzca la cadena a analizar: "; cin >> palabra;
14
n = palabra.length(); i = 0;
15
CurrentState = q0;
16
while (i<=n-1) {
17
switch(CurrentState) {
18
19
case q0:
20
if (palabra[i]==a) CurrentState=q1;
21
else if (palabra[i]==b || palabra[i]==c) CurrentState=q2;
22
else CurrentState=error;
23
break;
24
25
case q1:
26
if (palabra[i]==a || palabra[i]==b) CurrentState=q1;
27
else if (palabra[i]==c) CurrentState=q3;
28
else CurrentState=error;
29
break;
30
31
case q2:
32
if ((palabra[i]==a) || (palabra[i]==b) || (palabra[i]==c
))
CurrentState=q2;
else CurrentState=error;
break;
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
case q3:
if ((palabra[i]==a) || (palabra[i]==b) || (palabra[i]==c
))
CurrentState=q3;
else CurrentState=error;
break;
default:
if (!mensaje) {
cout << "Error alfabeto no valido" << endl;
cout << palabra[i-1] << endl;
mensaje=1;
}
}
i++;
if ((CurrentState==q1) || (CurrentState==q2))
cout << "La cadena " + palabra + " pertenece al lenguaje." <<
endl;
else cout << "La cadena " + palabra + " NO pertenece al lenguaje.
" << endl;
56
57
return 0;
58 }
1 #include <iostream>
2 #include <string>
3
4 using namespace std;
5
C28
[896]
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
52
53
54
55
56
57
58
59
default:
60
if (!mensaje) {
61
cout << "Error alfabeto no valido" << endl;
62
cout << palabra[i-1] << endl;
63
mensaje=1;}
64
}
65 }
66
67 int main() {
68
automata ejemplo;
69
StateType State;
70
71
cout<<"Introduzca la cadena a analizar: ";
72
cin>>palabra;
73
n = palabra.length();
[897]
74
75
76
77
78
79
80
81
82
83
84
85
86
87 }
i = 0;
ejemplo.Inicializar();
while (i<=n-1) {
ejemplo.UpdateState();
i++;
}
State=ejemplo.Informar();
if ((State==q1) || (State==q2))
cout << "La cadena " + palabra + " pertenece al lenguaje." <<
endl;
else cout << "La cadena " + palabra + " NO pertenece al lenguaje.
" << endl;
return 0;
Se ha creado una clase llamada automata, que se usar para crear un objeto
autmata. Esta clase contiene una variable privada, CurrentState, que almacenar
el estado en el que se encuentra el autmata en cada momento. A esta variable solo se podr acceder desde las funciones miembro de la clase, que son: Inicializar(),
ChangeState(), UpdateState() y Informar(). La funcin Inicializar(() es una funcin empleada para poner al autmata en el estado inicial. La funcin ChangeState()
es empleada para cambiar el estado en el que se encuentra el autmata.
La funcin Informar() devuelve el estado en el que est el autmata en un instante
cualquiera. Por ltimo, la funcin UpdateState() es la que implementa la tabla de
transiciones del autmata. Para conseguir el funcionamiento deseado lo que se har
ser crear un objeto de la clase automata, posteriormente ser inicializado, luego se
consultar la tabla de transiciones (ejecuciones de la funcin UpdateState()) mientras
la cadena que se est analizando tenga elementos, momento en el que se consultar el
estado en el que se queda el autmata tras leer toda la cadena para chequear si es un
estado de aceptacin o no.
En todas las implementaciones, la cadena de entrada ser aceptada si, una vez
leda, el estado que se alcanza es uno perteneciente al conjunto de estados nales. En
caso contrario, la cadena no ser un elemento del lenguaje que reconoce el autmata.
28.5.4.
Los ci que aparecen en el autmata mostrado son estados, cada uno de los cuales
tiene asociado una funcionalidad, o en el caso de los videojuegos un comportamiento,
diferente. El autmata que se muestra en la gura 28.80 tiene 4 estados y su
funcionamiento contar por lo tanto con otros tantos comportamientos {c0 , c1 , c2 , c3 }.
Las rij son reglas que son disparadas ante estmulos que ocurren en el entorno y que le
permiten cambiar de estado y por lo tanto modicar su comportamiento. Por ejemplo,
si estando en el estado c0 se satisfacen unas condiciones en el entorno que permite el
disparo de la regla r01 , esto provocar un cambio de estado, del estado c0 al estado
c1 y por lo tanto un cambio de comportamiento. Tambin se puede observar como
desde el estado c3 podran dispararse dos reglas diferentes r31 y r32 , en estos casos
los estmulos que disparan cada regla deben ser diferentes, se recuerda la necesidad
de estar en un nico estado en cada instante del funcionamiento del autmata (son
deterministas).
C28
[898]
Los juegos que son desarrollados usando lenguajes de alto nivel, como por ejemplo
C o C++, tpicamente almacenan todos los datos relacionados a cada agente en una
estructura o clase. Esta estructura o clase puede contener variables para almacenar
informacin como la posicin, vida, fuerza, habilidades especiales, y armas, entre
muchas otras. Por supuesto, junto a todos estos elementos, en la estructura o clase
tambin se almacenar el estado actual del autmata, y ser ste el que determine el
comportamiento del agente.
En el siguiente cdigo se muestra cmo se podra almacenar los datos que denen
al agente dentro de una clase.
Listado 28.27: Clase contenedora de los datos de un agente.
1 class AIEntity
2 {
3
public:
4
int type;
5
int state;
6
int row;
7
int column;
8
int health;
9
int strength;
10
int intelligence;
11
int magic;
12 };
r32
r01
r11
c0
c2
r31
c3
r21
Para implementar este tipo de mquinas de estado nito dotadas con salida, o
ms concretamente agentes como los comentados en la seccin anterior para su uso
en videojuegos, existen varias alternativas. stas estn basadas en las que ya se
han presentado en la seccin anterior pero con mejoras. La manera ms simple es
por medio de mltiples sentencias If-Else o la mejora inmediata al uso de estas
sentencias, por medio de una sentencia switch. La sentencia switch(estado_actual)
es una sentencia de seleccin. Esta sentencia permite seleccionar las acciones a
realizar de acuerdo al valor que tome la variable estado_actual, habr un case
para cada estado posible. La forma general de usar esta sentencia para implementar
una mquina de estados es la siguiente:
Listado 28.28: Implementacin del autmata por medio de sentencias switch.
[899]
1 switch (estado_actual)
2 {
3
case estado 1:
4
Funcionalidad del comportamiento del estado 1;
5
Transiciones desde este estado;
6
break;
7
case estado 2:
8
Funcionalidad del comportamiento del estado 2;
9
Transiciones desde este estado;
10
break;
11
case estado N:
12
Funcionalidad del comportamiento del estado N;
13
Transiciones desde este estado;
14
break;
15
default:
16
Funcionalidad por defecto;
17 }
case Inactivo:
Regenerar();
if (Temporizador(2)) ChangeState(Persiguiendo);
break;
C28
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
[900]
Inactivo};
2
3 void Soldier::UpdateState(StateType CurrentState)
4 {
5
switch(CurrentState)
6
{
7
case Patrullando:
8
Patrullar();
9
if (ObjetivoLocalizado()) ChangeState(Persiguiendo);
10
else (AtaqueRecibido()) ChangeState(Huyendo);
11
break;
12
13
case Persiguiendo:
14
Perseguir();
15
if (ObjetivoVulnerable()) ChangeState(Disparando);
16
else ChangeState(Huyendo);
17
break;
18
19
case Disparando:
20
if (ObjetivoAbatible()) Disparar();
21
else ChangeState(Persiguiendo);
22
break;
23
24
case Huyendo:
25
Huir();
26
if (PuestoaSalvo()) ChangeState(Patrullando);
27
else if (Abatido()) ChangeState(Inactivo);
28
break;
29
30
case Inactivo:
31
Regenerar();
32
if (TranscurridoTiempo()) ChangeState(Patrullando);
33
break;
34
}
35 }
Aunque a primera vista, este enfoque puede parecer razonable por lo menos en
autmatas no complejos, cuando se aplica a juegos, o mejor an, a comportamientos
de objetos ms complejos, la solucin presentada se convierte en un largo y tortuoso
camino. Cuantos ms estados y condiciones se tengan que manejar, ms riesgo hay de
convertir este tipo de estructura en cdigo spaghetti (vea la gura 28.82), por lo que
el ujo del programa ser difcil de entender y la tarea de depuracin una pesadilla.
Adems, este tipo de implementacin es difcil de extender ms all del propsito de
[901]
Estado
Inactivo
Persiguiendo
Huyendo
Huyendo
Condicin
Regenerado
PastillaComida
TranscurridoTiempo
Comido
Estado_Destino
Persiguiendo
Huyendo
Persiguiendo
Inactivo
Cuadro 28.6: Tabla de Transicin para el autmata implementado para simular el comportamiento del
fantasma en el juego del Comecocos (vea Figura 28.78.
#include <iostream>
#include <string>
using namespace std;
#define N 4 //numero de filas de la tabla de transicion
#define M 4 //numero de columnas de la tabla de transicion
C28
[902]
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
TablaTrans[0][1]
TablaTrans[0][3]
TablaTrans[1][1]
TablaTrans[1][3]
TablaTrans[2][1]
TablaTrans[2][3]
TablaTrans[3][1]
TablaTrans[3][3]
StateType ghost::Informar(void) {
return CurrentState;
}
void ghost::ChangeState(StateType State) {
CurrentState=State;
}
void ghost::UpdateState(Rule Transicion) {
StateType NextState;
NextState=TablaTrans[CurrentState][Transicion];
ChangeState(NextState);
switch(CurrentState) {
case huyendo:
Huir();
break;
case persiguiendo:
Perseguir();
break;
case inactivo:
Regenerar();
break;
default: Error();
}
/* Funciones de prueba */
void ghost::Huir(void) {
cout << "El fantasma huye!" << endl;
}
void ghost::Perseguir(void) {
cout << "El fantasma te persigue!" << endl;
}
=
=
=
=
=
=
=
=
error;
error;
huyendo;
error;
error;
inactivo;
error;
error;
[903]
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
void ghost::Regenerar(void) {
cout << "El fantasma se regenera!" << endl;
}
void ghost::Error(void) {
cout << "El fantasma esta en un estado de error!" << endl;
}
int main() {
ghost shadow;
StateType State;
string palabra;
shadow.Inicializar();
cout<<"Introduzca la regla: ";
cin>>palabra;
while (palabra.compare("salir")!=0) {
if (palabra.compare("regenerado")==0)
shadow.UpdateState(regenerado);
else if (palabra.compare("pastillacomida")==0)
shadow.UpdateState(pastillacomida);
else if (palabra.compare("transcurridotiempo")==0)
shadow.UpdateState(transcurridotiempo);
else if (palabra.compare("comido")==0)
shadow.UpdateState(comido);
else cout << "Accion no valida" << endl;
cout<<"Introduzca la regla: ";
cin>>palabra;
}
State=shadow.Informar();
if (State==error)
cout << "El fantasma no funciona bien." << endl;
else cout << "El fantasma funciona perfecto." << endl;
}
return 0;
Patrn de diseo
Un patrn de diseo es una solucion
simple y elegante a un problema especco y comn en el diseo orientado a objetos, que est basada en
la experiencia y su funcionamiento
ha sido demostrado. Su objetivo es
permitir la construccin de software
orientado a objetos exible y fcil
de mantener.
El n que se prentende lograr con la sentencia switch es controlar el comportamiento en funcin del estado. En el mundo orientado a objetos un cambio de comportamiento implica un tipo diferente de objeto. Por lo tanto, en este caso, lo que se
necesita es un objeto que parezca cambiar su tipo en funcin de su estado interno. Lo
que se va a hacer es intentar describir el comportamiento de un objeto como una coleccin de estados, de manera que sea el propio objeto el que maneje su comportamiento
mediante transiciones de un estado a otro. Para ello se va a hacer uso del patrn de
diseo State, cuyo diagrama UML es mostrado en la gura 28.83. El patrn de diseo
state es una solucin al problema de cmo hacer que un comportamiento dependa del
estado.
C28
Hasta ahora, la mayora de las implementaciones de autmatas que se han presentado hacen uso de la sentencia switch, que cuando son usadas para modelar comportamientos no muy complejos parece ms que adecuado. Sin embargo esta sentencia,
que permite una programacin tipo goto, se complica cuando los comportamientos
son complejos. En estos casos se requerirn muchas ramas, una por estado, y cada una
de ellas tendr largas sentencias condicionales para establecer las transisiones. Cada
vez que deba ser cambiada la sentencia switch, porque sea necesario aadir o eliminar
algn estado, por lo general acarrear un montn de cambios en el resto de la implementacin. A continuacin se muestra una posible solucin, basada en objetos, para la
implementacin de autmatas, y por lo tanto de agentes, que evita la sentencia switch.
[904]
La idea de este patrn es tener una clase, Contexto (Context class), que tendr
un comportamiento dependiendo del estado actual en el que se encuentre, es decir un
comportamiento diferente segn su estado. Tambin tiene una clase Estado abstracta
(State Class) que dene la interfaz pblica de los estados. En ella, se pondrn
todas las funciones del Contexto cuyo comportamiento puede variar. Luego hay una
implementacin de los estados concretos, que heredarn de la clase Estado abstracto.
La clase Contexto, poseer un puntero sobre el estado actual, que ser almacenado en
una variable miembro de la clase. Cuando se desea que el Contexto cambie de estado,
solo hay que modicar el estado actual.
El uso de este patrn implicar:
Denir una clase contexto para presentar una interfaz simple al exterior.
Denir una clase base abstracta estado.
Representar los diferentes estados del autmata como clases derivadas de la
clase base estado.
Denir la conducta especca de cada estado en la clase apropiada de las
denidas en el paso anterior (derivadas de la clase abstracta estado).
Mantener un puntero al estado actual en la clase contexto.
Para cambiar el estado en el que se encuentra el autmata, hay que cambiar el
puntero hacia el estado actual.
La implementacin del autmata que simula el comportamiento del fantasma en el
juego del Comecocos empleando el patrn de diseo State se muestra en el siguiente
listado de cdigo.
[905]
El patrn de diseo State no especica donde son denidas las transiciones entre
estados. Esto se puede hacer de dos formas diferentes: en la clase Context, o en cada
estado individual derivado de la clase State. En el cdigo mostrado anteriormente
se ha realizado de esta segunda forma. La ventaja de hacerlo as es la facilidad que
ofrece para aadir nuevas clases derivadas de la clase State. Su inconveniente es
que cada estado derivado de la clase State tiene que conocer como se conecta con
otras clases, lo cual introduce dependencias entre las subclases.
A modo de resumen, conviene indicar que no hay una nica manera de decribir
estados. Dependiendo de la situacin podra ser suciente con crear una clase abstracta
llamada State que posea unas funciones Enter(), Exit() y Update(). La creacin de
los estados particulares del autmata que se quiera implementar implicara crear una
subclase de la clase State por cada uno de ellos.
Listado 28.32: Implementacin del autmata que simula el comportamiento del fantasma en el
juego del Comecocos.
#include <iostream>
#include <string>
using namespace std;
class FSM;
// Clase base State.
// Implementa el comportamiento por defecto de todos sus mtodos.
class FSMstate {
public:
virtual void regenerado( FSM* ) {
cout << "
No definido" << endl; }
virtual void pastillacomida( FSM* ) {
cout << "
No definido" << endl; }
virtual void transcurridotiempo( FSM* ) {
cout << "
No definido" << endl; }
virtual void comido( FSM* ) {
cout << "
No definido" << endl; }
protected:
void changeState(FSM*, FSMstate*);
};
// Automa context class. Reproduce el intefaz de la clase State y
// delega todo su comportamiento a las clases derivadas.
class FSM {
public:
FSM();
void regenerado()
{ _state->regenerado(this); }
void pastillacomida()
{ _state->pastillacomida(this); }
void transcurridotiempo() { _state->transcurridotiempo(this); }
void comido()
{ _state->comido( this ); }
private:
friend class FSMstate;
void changeState( FSMstate* s )
FSMstate* _state;
};
{ _state = s; }
C28
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
[906]
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
private:
static FSMstate* _instance; };
FSMstate* Inactivo::_instance = 0;
class Persiguiendo : public FSMstate {
public:
static FSMstate* instance() {
if ( ! _instance ) _instance = new Persiguiendo;
cout << "El fantasma te persigue!" << endl;
return _instance; };
virtual void pastillacomida( FSM* );
private:
static FSMstate* _instance; };
FSMstate* Persiguiendo::_instance = 0;
class Huyendo : public FSMstate {
public:
static FSMstate* instance() {
if ( ! _instance ) _instance = new Huyendo;
cout << "El fantasma huye!" << endl;
return _instance; };
virtual void transcurridotiempo( FSM* );
virtual void comido( FSM* );
private:
static FSMstate* _instance; };
FSMstate* Huyendo::_instance = 0;
void Inactivo::regenerado( FSM* fsm ) {
cout << "Cambio de Estado a Persiguiendo." << endl;
changeState( fsm, Persiguiendo::instance()); };
void Persiguiendo::pastillacomida( FSM* fsm ) {
cout << "Cambio de Estado a Huyendo." << endl;
changeState( fsm, Huyendo::instance()); };
void Huyendo::transcurridotiempo( FSM* fsm ) {
cout << "Cambio de Estado a Persiguiendo." << endl;
changeState( fsm, Persiguiendo::instance()); };
void Huyendo::comido( FSM* fsm ) {
cout << "Cambio de Estado a Inactivo." << endl;
changeState( fsm, Inactivo::instance()); };
// El estado de comienzo es Inactivo
FSM::FSM() {
cout << "Inicializa:" << endl;
changeState( Inactivo::instance() );
}
int main() {
FSM
ghost;
string input;
cout<<"Introduzca regla: ";
cin>> input;
while (input.compare("salir")!=0) {
if (input.compare("regenerado")==0)
ghost.regenerado();
else if (input.compare("pastillacomida")==0)
ghost.pastillacomida();
else if (input.compare("transcurridotiempo")==0)
ghost.transcurridotiempo();
else if (input.compare("comido")==0)
ghost.comido();
else cout << "Regla no existe, intente otra" << endl;
cout<<"Introduzca regla: ";
[907]
122
cin>> input;
123
}
124 }
C28
[908]
c0
c1
c02
r01
r11
c01
c03
r32
r31
c11
c04
c0
c12
r21
[909]
28.5.5.
C28
La modelizacin de comportamientos basados en aproximaciones reactivas pueden provocar que los personajes del juego muestren comportamientos repetitivos y
por ello predecibles. Las aproximaciones deliberativas otorgan mayor libertad a estos
personajes, ya que les premiten adaptar sus acciones para que se ajusten a la situacin
en la que se encuentran.
[910]
Huir
Estado Inicial
Correr
Coger
Arma
Descansar
Cargar
Arma
Ponerse a
salvo
Apuntar
Curarse
Disparar
Estado Objetivo
Jugador Abatido
[911]
El plan se ejecutar hasta que se complete, se invalide o hasta que haya un cambio
de objetivo. Si cambia el objetivo o no se puede continuar con el plan actual por alguna
razn, se aborta el plan actual y se genera uno nuevo. Existe tambin la posibilidad de
que cuando exista ms de un plan vlido, el planicador obtenga el de menor coste,
considerando que cada accin acarrea un coste. En cierto modo, tras todo este proceso
lo que hay es una bsqueda de un camino o del camino de menor coste.
En la gura 28.85 se muestra un plan para matar al jugador, en el se pasa del estado
inicial al estado objetivo mediante las acciones: Coger Arma, Cargar Arma, Apuntar
y Disparar.
Con el uso de GOAP:
Se consigue dotar a los personajes de comportamientos adaptativos. La capacidad de generacin de planes permite ajustar sus acciones a su entorno actual, y
encontrar diferentes soluciones a un problema.
Se consigue comportamientos ms simples de disear. Es el planicador quien
se encarga de analizar las posibles alternativas existentes y encontrar la mejor
opcin, no hay que prever para cada situacin posible la accin a realizar.
Se consigue comportamientos variables. Los planes no estn denidos de
antemano, se elaboran en tiempo de ejecucin dependiendo del estado actual
del entorno.
Ejemplos de juegos que emplean GOAP son:
Mushroom Men: The Spore Wars (Wii) - Red Fly Studio, 2008
Los Cazafantasmas (Wii) - Red Fly Studio, 2008
Silent Hill: Homecoming (X360/PS3) - Double Helix Games / Konami de 2008
Fallout 3 (X360/PS3/PC) - Bethesda Softworks, 2008
Empire: Total War (PC) - Creative Assembly / Sega, 2009
FEAR 2: Project Origin (X360/PS3/PC) - Monolith Productions Bros / Warner,
2009
Demigod (PC) - Gas Powered Games / Stardock, 2009
Just Cause 2 (PC/X360/PS3) - Avalanche Studios / Eidos Interactive, 2010
Transformers: War for Cybertron (PC/X360/PS3) - High Moon Studios /
Activision, 2010
Trapped Dead (PC) - Juegos Headup, 2011
Deus Ex: Human Revolution (PC/X360/PS3) - Eidos Interactive, 2011
La tendencia actual en el desarrollo de videojuegos es usar planicadores
jerrquicos. Un planicador jerrquico primero formula un plan abstracto expresado
en trminos de tareas de alto nivel, por lo que no puede realizarse directamente.
Despus rena dicho plan con el objetivo de producir un plan completo en tminos
de operadores bsicos, o acciones que el agente pueda realizar directamente.
C28
[912]
Abatir al Jugador
Armarse
Coger
Arma
Apuntar
Disparar
Cargar
Arma
Figura 28.87: Planicador jerrquico.
28.5.6.
El algoritmo de bsqueda A* y la Mquina Finita de Estados (FSM) son probablemente las dos tcnicas ms empleadas de la IA en el desarrollo de videojuegos,
para establecer caminos o rutas y para modelar el comportamiento de un personaje,
respectivamente.
No obstante, la IA cada vez tiene ms importancia en el desarrollo de videojuegos
(vea la gura 28.88). Puesto que cada vez se podr disponer de computacin adicional,
esto da pie a dedicar un procesador, o parte de l, a la IA y aplicar tcnicas
ms avanzadas (Lgica Difusa, Computacin Evolutiva, Redes Neuronales, Modelos
Probabilsticos,. . . ).
[913]
28.6.
C28
En el ao 2050...
[914]
28.6.1.
Introduccin a Pygame
3 http://sourceforge.net/projects/sserver/
4 http://pygame.org
[915]
Pygame es altamente portable y, por lo tanto, se puede ejecutar sobre una gran
cantidad de plataformas y sistemas operativos. Adems, mantiene una licencia LGPL5 ,
permitiendo la creacin de open source, free software, shareware e incluso juegos
comerciales.
A continuacin se enumeran las principales caractersticas de Pygame:
Gran nivel funcional, debido a la continuidad del proyecto.
Independencia respecto a la biblioteca grca de bajo nivel, no siendo necesario
utilizar exclusivamente OpenGL.
Soporte multi-ncleo.
Uso de cdigo C optimizado y de ensamblador para la implementacin de la
funcionalidad del ncleo de Pygame.
Alta portabilidad.
Facilidad de aprendizaje.
Amplia comunidad de desarrollo.
Autonoma para controlar el bucle principal de juego.
No es necesaria una interfaz grca para poder utilizar Pygame.
Sencillez en el cdigo, debido a la simplicidad de Pygame.
Al igual que ocurre en SDL, uno de los componentes fundamentales en Pygame
est representado por el concepto de supercie (surface)6 , debido a que es el objeto
contemplado por Pygame para la representacin de cualquier tipo de imagen. En la
tabla 28.7 se resume la lista de mdulos que conforman Pygame junto a una breve
descripcin.
La instalacin de Pygame en Debian GNU/Linux es trivial mediante los siguientes
comandos:
$ sudo apt-get update
$ sudo apt-get install python-pygame
5 http://www.pygame.org/LGPL
6 http://www.pygame.org/docs/ref/surface.html
C28
1
2
3
4
5
6
7
8
[916]
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Por otra parte, el mtodo render() contiene las primitivas de Pygame necesarias
para dibujar los aspectos bsicos del terreno de juego. Note cmo se hace uso de las
primitivas geomtricas rect, circle y line, respectivamente, para dibujar las lneas de
fuera y el centro del campo. Adems de especicar el tipo de primitiva, es necesario
indicar el color y, en su caso, el grosor de la lnea.
Tpicamente, el mtodo render() se ejecutar tan rpidamente como lo permitan
las prestaciones hardware de la mquina en la que se ejecute el ejemplo, a no ser que
se establezca un control explcito del nmero de imgenes renderizadas por segundo.
El siguiente listado de cdigo muestra cmo inicializar Pygame (lnea 14) e
instanciar el terreno de juego (lnea 17). El resto de cdigo, es decir, el bucle innito
permite procesar los eventos recogidos por Pygame en cada instante de renderizado.
Note cmo en cada iteracin se controla si el usuario ha cerrado la ventana grca,
capturado mediante el evento QUIT, y se lleva a cabo el renderizado del terreno de
juego (lnea 27) junto con la actualizacin de Pygame (lnea 29).
La gestin de la tasa de frames generados por segundo es simple mediante la clase
Clock. De hecho, slo es necesario especicar la tasa de frames deseada mediante la
funcin clock.tick(FPS) para obtener una tasa constante.
Listado 28.34: Ejecutando el ejemplo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Figura 28.91: Resultado del renderizado de un terreno de juego simplicado mediante Pygame.
Event-oriented
La combinacin de un esquema basado en eventos y una arquitectura
con varias capas lgicas facilita la
delegacin y el tratamiento de eventos.
[917]
Mdulo
camera
cdrom
color
cursors
display
draw
event
examples
font
gfxdraw
image
joystick
key
locals
mask
midi
mixer
mouse
movie
music
overlay
pixelarray
pygame
rect
scrap
sndarray
sprite
surface
surfarray
tests
time
transform
Breve descripcin
Soporte para el uso de cmaras
Manejo de unidades de CD/DVD
Representacin de colores
Soporte y gestin de cursores
Control sobre dispositivos y ventanas
Renderizado de formas sobre una supercie
Interaccin con eventos
Mdulo con ejemplos bsicos
Carga y renderizado de fuentes
Renderizado de formas sobre una supercie
Carga y almacenamiento de imgenes
Interaccin con joysticks
Interaccin con el teclado
Constantes usadas por Pygame
Soporte de mscaras sobre imgenes
Interaccin con dispositivos midi
Carga y reproduccin de sonido
Interaccin con el ratn
Carga de vdeo mpeg
Control de streaming de audio
Soporte para overlays grcos
Acceso directo a pxeles en supercies
Paquete de alto nivel de Pygame
Objeto para gestionar rectngulos
Soporte para clipboards
Acceso a samples de sonido
Mdulo con objetos bsicos
Objeto para representar imgenes
Acceso a pxeles de supercies mediante arrays
Paquete para pruebas unitarias
Monitorizacin de tiempo
Transformacin de supercies
28.6.2.
Recuerde que la simplicidad de una
solucin facilita la mantenibilidad
del cdigo y el desarrollo de nuevos
componentes basados en los previamente desarrollados.
C28
Simplicacin
Arquitectura propuesta
[918]
La gura 28.93 muestra, desde un punto de vista de alto nivel, el diagrama de clases de la arquitectura propuesta. Como se puede apreciar, la clase SoccerField aparece
en la parte superior de la gura y est compuesta de dos equipos (SoccerTeam), dos
porteras (SoccerGoal), un baln (SoccerBall) y un determinado nmero de regiones
(SoccerRegion).
Por otra parte, el equipo de ftbol est compuesto por un determinado nmero de
jugadores, cuatro en concreto, modelados mediante la clase SoccerPlayer, que a su
vez mantiene una relacin de herencia con la clase MovingEntity. A continuacin se
realizar un estudio ms detallado de cada una de estas clases.
El terreno de juego
Una de las clases principales de la arquitectura propuesta es la clase SoccerField,
la cual mantiene como estado interno referencias a los elementos ms importantes que
forman parten de dicha arquitectura. Estos elementos son los siguientes:
surface, de tipo pygame.surface, que representa la supercie de Pygame
utilizada para renderizar cada uno de los elementos que forman parte del juego.
playing_area, de tipo pygame.rect, que representa el terreno real de juego
considerando las lneas de fuera de banda y de crner.
walls, de tipo lista, que contiene cuatro instancias de la clase Wall2d, las cuales
representan las dos bandas y las dos lneas de crner. Estas estructuras se
utilizan para detectar cuando el baln sale del terreno de juego.
regions, de tipo lista, que contiene estructuras del tipo SoccerRegion que
facilitan la divisin del terreno de juego en distintas zonas para gestionar
aspectos como la posicin inicial de los jugadores o la confeccin de estrategias
de equipo.
[919]
SoccerField
1..n
SoccerRegion
SoccerGoal
SoccerTeam
SoccerBall
SoccerPlayer
MovingEntity
ball, de tipo SoccerBall, que representa al baln usado para llevar a cabo las
simulaciones.
teams, de tipo diccionario, que permite indexar a dos estructuras de tipo
SoccerTeam a partir del identicador del equipo (red o blue).
goals, de tipo diccionario, que permite indexar a dos estructuras de tipo
SoccerGoal a partir del identicador del equipo (red o blue).
El siguiente listado de cdigo muestra la parte relativa a la inicializacin de
la clase SoccerField. Note cmo en el mtodo constructor (__init()__) se lleva a
cabo la instanciacin de los elementos principales que conforman la simulacin.
Como se estudiar ms adelante, algunas de estos elementos mantienen relaciones de
agregacin con otros elementos, como por ejemplo ocurre entre la clase SoccerTeam
y la clase SoccerPlayer (un equipo est compuesto por varios jugadores).
Recuerde inicializar completamente el estado de una instancia en el
proceso de creacin. En el caso de
Python, el constructor est representado por el mtodo __init()__.
C28
Inicializacin
[920]
3
4 class SoccerField:
5
6
def __init__ (self, width, height):
7
8
self.width, self.height = width, height
9
10
self.surface = pygame.display.set_mode(
11
(self.width, self.height), 0, 32)
12
# Terreno real de juego.
13
self.playing_area = Rect(TOP_LEFT, WIDTH_HEIGHT)
14
15
# Walls.
16
self.walls = []
17
# Clculo de coordenadas clave del terreno de juego.
18
self.walls.append(Wall2d(top_left, top_right))
19
self.walls.append(Wall2d(top_right, bottom_right))
20
self.walls.append(Wall2d(top_left, bottom_left))
21
self.walls.append(Wall2d(bottom_left, bottom_right))
22
23
# Zonas importantes en el campo.
24
self.regions = {}
25
# Clculo de las regiones...
26
27
# Bola.
28
self.ball = SoccerBall(Vec2d(self.playing_area.center),
29
10, 2, Vec2d(0, 0), self)
30
31
# Equipos
32
self.teams = {}
33
self.teams[red] = SoccerTeam(red, RED, 4, self)
34
self.teams[blue] = SoccerTeam(blue, BLUE, 4, self)
35
36
# Porteras.
37
self.goals = {}
38
self.goals[red] = SoccerGoal(
39
red, RED, self.playing_area.midleft, self)
40
self.goals[blue] = SoccerGoal(
41
blue, BLUE, self.playing_area.midright, self)
En la gura 28.94 se muestra la divisin lgica del terreno de juego en una serie
de regiones con el objetivo de facilitar la implementacin del juego simulado, los
comportamientos inteligentes de los jugadores virtuales y el diseo de las estrategias
en equipo.
Internamente, cada regin est representada por una instancia de la clase SoccerRegion, cuya estructura de datos ms relevante es un rectngulo que dene las
dimensiones de la regin y su posicin.
Aunque cada jugador conoce en cada momento su posicin en el espacio 2D, como
se discutir ms adelante, resulta muy til dividir el terreno de juego en regiones para
implementar la estrategia interna de cada jugador virtual y la estrategia a nivel de
equipo. Por ejemplo, al iniciar una simulacin, y como se muestra en la gura 28.94,
cada jugador se posiciona en el centro de una de las regiones que pertenecen a su
campo. Dicha posicin se puede reutilizar cuando algn equipo anota un gol y los
jugadores han de volver a su posicin inicial para efectuar el saque de centro.
Listado 28.36: Clase SoccerField. Mtodo render
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*3
4 class SoccerField:
5
6
def render (self):
7
Ms info...
El uso de informacin o estructuras
de datos adicionales permite generalmente optimizar una solucin y
plantear un diseo que sea ms sencillo.
[921]
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
C28
[922]
Figura 28.94: Divisin del terreno de juego en regiones para facilitar la integracin de la IA.
16
17
18
19
20
21
22
23
24
25
heading, mass):
self.pos = pos
self.radius = radius
self.velocity = velocity
self.max_speed = max_speed
self.heading = heading
# Vector perpendicular al de direccin (heading).
self.side = self.heading.perpendicular
self.mass = mass
Dentro de la clase SoccerBall existen mtodos que permiten, por ejemplo, detectar
cundo el baln sale fuera del terreno de juego. Para ello, es necesario comprobar,
para cada uno de los laterales, la distancia existente entre el baln y la lnea de fuera.
Si dicha distancia es menor que un umbral, entonces el baln se posicionar en la
posicin ms cercana de la propia lnea para efectuar el saque de banda.
Listado 28.38: Clase SoccerBall. Mtodo testCollisionWithWalls
1 def testCollisionWithWalls (self):
2
3
# Iterar sobre todos los laterales del campo
4
# para calcular si el baln interseca.
[923]
for w in self.walls:
if distToWall(w.a, w.b, self.pos) < TOUCHLINE:
self.reset(self.pos)
Por otra parte, el baln sufrir el efecto del golpeo por parte de los jugadores
virtuales mediante el mtodo kick(). Bsicamente, su funcionalidad consistir en
modicar la velocidad del baln en base a la fuerza del golpeo y la direccin del
mismo.
Listado 28.39: Clase SoccerBall. Mtodo kick
1 # Golpea el baln en la direccin dada.
2 def kick (self, direction, force):
3
4
# Normaliza la direccin.
5
direction = direction.normalized()
6
# Calculo de la aceleracin.
7
acceleration = (direction * force) / self.mass
8
# Actualiza la velocidad.
9
self.velocity = acceleration
pi
Tambin resulta muy interesante incluir la funcionalidad necesaria para que los
jugadores puedan prever la futura posicin del baln tras un intervalo de tiempo. Para
llevar a cabo el clculo de esta futura posicin se ha de obtener la distancia recorrida
por el baln en un intervalo de tiempo t, utilizando la ecuacin 28.7,
distancia cubierta
1
x = ut + at2
2
(28.7)
C28
[924]
El jugador virtual
La entidad base para representar a cualquier jugador de ftbol virtual est
representada por la clase SoccerPlayer, la cual a su vez es una especializacin de
la clase MovingEntity (al igual que el baln de juego). Esta entidad base se podr
extender para especializarla en funcin de distintos tipos de jugadores, tpicamente
jugadores de campo y portero.
El siguiente listado de cdigo muestra una posible implementacin bsica de esta
clase. Note cmo en el mtodo update() se actualiza la velocidad interna del jugador
en funcin de los comportamientos que tenga activos, tal y como se discute en la
siguiente seccin.
Listado 28.41: Clase SoccerPlayer
1 class SoccerPlayer (MovingEntity):
2
3
def __init__ (self, team, colour, number, pos, soccer_field):
4
# Inicializacin de MovingEntity.
5
self.team = team
6
self.colour = colour
7
self.number = number
8
self.initialPos = pos
9
self.soccer_field = soccer_field
10
aux = (soccer_field.playing_area.center - pos).normalized()
11
self.direction = aux
12
13
# Comportamientos asociados.
14
self.steeringBehaviours = SteeringBehaviours(
15
self, soccer_field.ball)
16
17
# Posicin inicial (e.g. kickoff).
18
def reset (self):
19
self.pos = self.initialPos
20
21
# Actualizacin de la posicin del jugador.
22
def update (self):
23
self.velocity = self.steeringBehaviours.calculate()
24
self.pos += self.velocity
28.6.3.
La gura 28.98 muestra el diagrama de clases con las distintas entidades involucradas en la integracin de IA dentro del simulador futbolstico. La parte ms relevante
de este diagrama est representado por la clase SteeringBehaviours, la cual aglutina
los distintos comportamientos asociados, desde un punto de vista general, al jugador
de ftbol. Note tambin cmo la clase base asociada al jugador de ftbol, SoccerPlayer, ha sido especializada en otras dos clases, FieldPlayer y Goalkeeper, con el
objetivo de personalizar el comportamiento de los jugadores de campo y el portero,
respectivamente.
[925]
8 http://opensteer.sourceforge.net/
C28
[926]
MovingEntity
SoccerPlayer
GoalKeeper
FieldPlayer
SoccerTeam
Figura 28.98: Diagrama de clases del modelo basado en comportamientos para la simulacin de ftbol.
El siguiente listado de cdigo muestra el mtodo constructor de la clase SteeringBehaviours. Como se puede apreciar, dicha clase incluye como estado un diccionario,
denominado activated, con los distintos tipos de comportamientos habilitados en la
misma que, inicialmente, no estn activados.
Listado 28.42: Clase SteeringBehaviours. Mtodo constructor
1 class SteeringBehaviours:
2
3
def __init__ (self, player, ball):
4
5
self.player = player
6
self.ball = ball
7
8
self.target = ball
9
10
# Comportamientos activados.
11
self.activated = {}
12
self.activated[seek] = False
13
self.activated[pursuit] = False
14
self.activated[arrive] = False
15
# Ms comportamientos aqu...
SteeringBehaviours
[927]
En el siguiente listado de cdigo se muestra la implementacin de los mtodos calculate(), sumForces() y truncate, utilizados para devolver la fuerza global resultante,
calcularla en base a los comportamientos activados y truncarla si as fuera necesario,
respectivamente.
Listado 28.43: Clase SteeringBehaviours. Clculo de fuerza
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
C28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[928]
15
16
17
18
19
20
21
22
23
24
25
if self.ball.velocity.get_length() != 0.0:
sc_velocity = self.ball.velocity.get_length()
lookAheadTime = toBall.get_length() / sc_velocity
# Dnde estar la bola en el futuro?
target = self.ball.futurePosition(lookAheadTime)
# Delega en el comportamiento arrive.
return self.arrive(target)
28.6.4.
El esquema bsico planteado hasta ahora representa la base para la implementacin del mdulo de IA asociado a los distintos jugadores del equipo virtual. En
otras palabras, el cdigo fuente base discutido hasta ahora se puede entender como un
framework sobre el que construir comportamientos inteligentes a nivel de jugador
individual y a nivel de estrategia de equipo.
Por ejemplo, la gura 28.99 muestra un ejemplo clsico en simuladores futbolsticos donde un jugador que no mantiene el baln en un determinado instante de tiempo,
es decir, un jugador de apoyo (supporting player) ha de emplazarse en una buena posicin para recibir el baln. Evidentemente, es necesario garantizar que no todos los
jugadores de un equipo que no mantienen el baln se muevan a dicha posicin, sino
que han de moverse a distintas posiciones para garantizar varias alternativas de pase.
En el ejemplo de la gura 28.99, el jugador 2 representa una mejor alternativa
de pase frente al jugador 3. Para evaluar ambas alternativas es necesario establecer
un mecanismo basado en puntuaciones para computar cul de ellas es la mejor (o la
menos mala). Para ello, se pueden tener en cuenta los aspectos como los siguientes:
Facilidad para llevar a cabo el pase sin que el equipo contrario lo intercepte.
Para ello, se puede considerar la posicin de los jugadores del equipo rival y el
tiempo que tardar el baln en llegar a la posicin deseada.
Probabilidad de marcar un gol desde la posicin del jugador al que se le quiere
pasar.
Histrico de pases con el objetivo de evitar enviar el baln a una posicin que
ya fue evaluada recientemente.
3
Figura 28.99: Esquema de apoyo
de jugadores. El jugador 2 representa una buena alternativa mientras que el jugador 3 representa una
mala.
[929]
28.6.5.
Figura 28.101: Esquema defensivo
basado en la colocacin de los jugadores del equipo que deende en
determinadas regiones clave.
Consideraciones nales
t i j
28.7.
28.7.1.
Introduccin
Los sistemas expertos son una rama de la Inteligencia Articial simblica que
simula comportamientos humanos razonando con comportamientos implementados
por el diseador del software. Los sistemas expertos se usan desde hace ms de 30
aos, siendo unos de los primeros casos de xito de aplicacin prctica de tcnicas de
Inteligencia articial. Existen diversos tipos: basados en casos, rboles de decisin,
basados en reglas, etc. Este ltimo tipo ser el que se aplicar al desarrollo de
inteligencia articial para un juego a lo largo del presente apartado.
C28
ti
[930]
[931]
Dado que el nmero de hechos que suele cambiar tras cada activacin de una regla
suele ser bajo, una adecuada organizacin de condicionantes permite, a nivel prctico,
dar unos tiempos de ejecucin adecuados. Si se desea ampliar informacin sobre este
asunto, el principal libro de referencia es [39].
28.7.2.
C28
Para implementar un sistema experto que pueda jugar a este juego se usar
CLIPS9 , un entorno para el desarrollo de sistemas expertos basados en reglas que
inicialmente fue desarrollado por la NASA y actualmente mantenido por uno de sus
creadores, Gary Riley. CLIPS est disponible bajo licencia libre en su web [75], y se
puede usar interactivamente desde consola o integrndolo con diversos lenguajes de
programacin (C/C++, Java, Python, etc).
[932]
6
7
8
9
10
11
12
13
14
15
16
17
A primera vista, esta regla puede parecer adecuada, al n y al cabo slo mueve una
cha hacia delante, que es donde estn los contrarios al comenzar la partida. Y como
su prioridad es bastante baja, slo se ejecutar si no hay otra regla de mayor prioridad
(que se supone har una accin mejor). Sin embargo es muy mejorable. Por ejemplo,
10 Los movimientos estn denidos como 1 avanza en X, 2 retrocede en X, 3 avanza en Y y 4 retrocede
en Y.
[933]
pudiera ser que la cha se dirigiera hacia una cha contraria de mayor puntuacin,
como podra ser la cha de 2 puntos del equipo prpura que est en la primera la
del tablero en la gura 28.106. Si se aplica la regla anterior a dicha cha se estaran
perdiendo varios turnos para suicidarla.
As que se podra cambiar la regla por la que aparece en el listado 28.47. En este
caso se han aadido dos condiciones nuevas. La primera (lnea 5) busca la existencia
de una cha contraria (equipo B) en la misma columna (pues es usa la misma
variable ?x1 para los dos campos pos-x en su lnea 5). Y la segunda condicin (lnea
7) comprueba que la cha contraria est arriba de la propia y que tenga menos valor
que esta. Ntese el uso de notacin preja, primero aparece el operador y despus los
operandos.
C28
Puede observarse que la primera vez que aparece una variable se instancia al
valor de un hecho del sistema, y a partir de ah el resto de apariciones en esa y otras
condiciones ya se considera ligada a una constante. En caso de que se cumplan todas
las condiciones, la regla se considera activable. Si alguna de las condiciones no puede
satisfacerse, se intenta satisfacer de nuevo instanciando las variables con otros hechos
del sistema.
[934]
10
11 )
Para evitar el suicidio hay que el elemento condicional not, que comprueba
la no existencia de hechos. Si se desea indicar la no existencia de hechos con
determinados valores constantes en sus campos simplemente se antepone not a la
condicin. Por ejemplo (not (cha (equipo A) (num 111))). Pero si se quieren incluir
comprobaciones ms complejas hay que incluir la conectiva Y, &. A continuacin
se muestra un ejemplo en que se aplicar la una regla de huida del listado 28.49,
quedando como ejercicio la aplicacin a las reglas anteriores de ataque.
Listado 28.49: Versin inicial de la regla huir
1 (defrule EQUIPO-A::huir
2
(declare (salience 20))
3
(ficha (equipo "A") (num ?n1) (pos-x ?x) (pos-y ?y1)
4
(puntos 1))
5
(ficha (equipo "B")
(pos-x ?x) (pos-y ?y2)
6
(puntos 5))
7
(test (> ?y1 ?y2))
8
9
(tiempo ?t)
10
=>
11
(assert (mueve (num ?n1) (mov 4) (tiempo ?t)))
12
13 )
Esta regla, sin embargo, adolece los problemas anteriormente comentados: posibilidad de suicidio y aplicacin incluso con chas contrarias muy lejanas (vase estado
de la gura 28.107). Se propone como mejora la regla del listado 28.50.
Listado 28.50: Versin nal de la regla huir
1 (defrule EQUIPO-A::huir5
2
(declare (salience 20))
3
(ficha (equipo "A") (num ?n1) (pos-x ?x) (pos-y ?y1)
4
(puntos 1))
5
(ficha (equipo "B")
(pos-x ?x) (pos-y ?y2)
6
(puntos 5))
7
(test (or (= ?y1 (+ ?y2 1)) (= ?y1 (- ?y2 1))))
8
(not (ficha (equipo "B") (pos-x ?x1) (pos-y (+ 1 ?y1))))
9
(not (ficha (equipo "B") (pos-x ?x1) (pos-y (- 1 ?y1))))
10
(tiempo ?t)
11
=>
12
(assert (mueve (num ?n1) (mov 1) (tiempo ?t)))
13
(assert (mueve (num ?n1) (mov 2) (tiempo ?t)))
14 )
[935]
hay veces que los programadores se plantean poner reglas que tengan condiciones
excluyentes u ordenar todas las reglas por prioridades. Aunque esta estrategia no es
necesariamente mala, se pierde el potencial de los sistemas expertos basados en reglas,
pues realmente se estara implementando un rbol de decisin determinista.
Otro aspecto interesante es que tambin se pueden crear otros hechos para
facilitar la planicacin de estrategias. Por ejemplo, si una estrategia cambia su
comportamiento cuando pierde las chas de 5 y 6 puntos se aadira la primera regla
del listado 28.51 y el hecho fase 2 se usara como condicin de las reglas exclusivas
de dicho comportamiento.
Listado 28.51: Regla de cambio de fase
1
2
3
4
5
6
7
8
9
10
11
12
13
(defrule EQUIPO-A::cambioDeFase
(declare (salience 90))
(not (ficha (equipo "A") (puntos 5)))
(not (ficha (equipo "A") (puntos 6)))
=>
(assert (fase 2))
)
(defrule EQUIPO-A::despistar
(declare (salience 50))
(fase 2)
...
)
C28
[936]
Captulo
29
Networking
Flix Jess Villanueva
David Villa Alises
29.1.
Internet es posible gracias al conjunto de protocolos denominado pila TCP/IP (Pila de protocolos de Internet). Un protocolo es un conjunto de reglas sintcticas, semnticas y de temporizacin que hacen posible la comunicacin entre dos procesos
cualesquiera.
En todo proceso de comunicaciones estn involucrados un conjunto de protocolos
cuyo cometido es muy especco y que tradicionalmente se han dividido en capas.
Estas capas nos sirven para, conceptualmente, resolver el problema complejo de las
comunicaciones aplicando el principio de divide y vencers.
937
[938]
Como podemos ver en la gura 29.1 cada funcionalidad necesaria se asocia a una
capa y es provista por uno o varios protocolos. Es decir, cada capa tiene la misin
especca de resolver un problema y, generalmente, existen varios protocolos que
resuelven ese problema de una forma u otra. En funcin de la forma en la cual resuelve
el problema el protocolo, dicha solucin tiene unas caractersticas u otras. En la capa
de aplicacin la funcionalidad es determinada por la aplicacin.
La agregacin de toda esta informacin dividida en capas se realiza mediante
un proceso de encapsulacin en el cual los protocolos de las capas superiores se
encapsulan dentro de las capas inferiores y al nal se manda la trama de informacin
completa. En el host destino de dicha trama de informacin se realiza el proceso
inverso. En la imagen 29.2 podemos ver un ejemplo de protocolo desarrollado para
un videojuego (capa de aplicacin) que utiliza UDP (User Datagram Protocol) (capa
de transporte), IPv4 (Capa de red) y nalmente Ethernet (capa de Acceso a Red).
En las siguientes secciones vamos a dar directrices para el diseo e implementacin del protocolo relacionado con el videojuego as como aspectos a considerar para
seleccionar los protocolos de las capas inferiores.
[939]
29.2.
Consideraciones de diseo
C29
[940]
[941]
Compresin de datos La compresin de datos entendida como las tcnicas destinadas a reducir el ancho de banda necesario para el juego multijugador. Entre las
tcnicas utilizadas podemos citar:
Actualizaciones incrementales, es decir, enviar a los clientes slo y
exclusivamente la informacin que ha cambiado con respecto a la ltima
actualizacin enviada. En esta tcnica, de forma general, cada cierto
tiempo se enva un estado completo del mundo para corregir posibles
prdidas de paquetes.
Seleccionar el destino de la informacin transmitida en funcin de qu
clientes se ven afectados por el cambio de estado de los objetos en lugar
de transmitir toda la informacin a todos.
agregacin: envo de datos comunes a varios jugadores con un solo
paquete gracias, por ejemplo, el uso de comunicaciones multicast.
Interpolacin La interpolacin permite obtener nuevos estados del mundo virtual del
juego en el cliente a partir de los datos recibidos del servidor. En funcin de la
complejidad de estos nuevos estados siempre son una aproximacin al estado
que tendramos si obtuviramos eventos de estado del servidor a una altsima
frecuencia. Esta tcnica nos permite actualizar el estado del juego en el cliente
mas a menudo que las actualizaciones enviadas por el SERVIDOR y, al mismo
tiempo, poder inferir informacin perdida en la red.
Prediccin La prediccin nos ayuda a obtener nuevos estados futuros del mundo
virtual del juego en el cliente a partir de los datos recibidos del servidor. La
diferencia respecto a la interpolacin es que la sta genera nuevos estados entre
dos estados recibidos por el servidor de cara a la renderizacin mientras que
la prediccin se adelanta a los eventos recibidos desde el servidor. Para ello, el
cliente puede predecir su movimiento en funcin de la entrada que recibe del
usuario y sin que el servidor se lo comunique.
Compensacin del retardo A pesar de las tcnicas anteriores siempre las acciones
cuentan con un retardo que se debe compensar, en la medida de lo posible,
guardando los eventos recibidos por los clientes y sincronizando el estado del
mundo de forma acorde a como estaba el mundo cuando el evento fue originado,
es decir, eliminando el intervalo de comunicacin.
Una vez implementadas estas tcnicas se debe estimar y medir los parmetros de
networking para ver la efectividad de las tcnicas, estudiar el comportamiento de la
arquitectura, detectar cuellos de botella, etc... Por lo tanto, las estimaciones de retardo
y en general, todos los parmetros de red deben ser incluidos en los tests.
Con estas tcnicas en mente, la responsabilidad del cliente, a grandes rasgos, queda
en un bucle innito mientras se encuentre conectado. En dicho bucle, para cada turno
o intervalo de sincronizacin, enva comandos de usuario al servidor, comprueba si
ha recibido mensajes del servidor y actualiza su visin del estado del juego si es as,
establece los clculos necesarios: prediccin, interpolacin, etc. y por ltimo muestra
los efectos al jugador: grcos, mapas, audio, etc.
El servidor por su parte, actualiza el estado del mundo con cada comando recibido
desde los clientes y enva las actualizaciones a cada uno de los clientes con los cambios
que se perciben en el estado del juego.
En ambos casos se necesita un algoritmo de sincronizacin que permita a clientes
y servidores tener consciencia del paso del tiempo. Existen varios algoritmos y
protocolos empleados para esta sincronizacin (a menudo heredados del diseo y
desarrollo de sistemas distribuidos).
C29
[942]
29.3.
29.3.1.
Peer to peer
juegos de esta poca utilizaban normalmente el protocolo IPX para las comunicaciones en LAN
[943]
29.3.2.
Cliente-servidor
29.3.3.
Pool de servidores
C29
[944]
29.4.
Cuando la red entra en juego debemos tener en cuenta tres limitaciones adicionales
adems de las habituales:
29.4.1.
Capacidad de cmputo
29.4.2.
Ancho de banda
Es la tasa de transferencia efectiva (en bits por segundo) entre los distintos computadores que alojan partes del juego. Si existen ujos asimtricos deben considerarse
por separado. Es tambin habitual que entre dos componentes existan ujos de control
y datos diferenciados. Resulta determinante cuando se deben soportar grandes cantidades de jugadores y es el aspecto que ms limita la escalabilidad de la arquitectura
cliente-servidor.
29.4.3.
Latencia
Es el tiempo que tarda un mensaje desde que sale del nodo origen hasta que llega
al nodo destino. Hay muchos factores que afectan a la latencia: las limitaciones fsicas
del medio, los procedimientos de serializacin de los mensajes, el sistema operativo,
el tiempo que los paquetes pasan en las colas de los encaminadores, los dispositivos
de conmutacin, etc.
Muy relacionado con la latencia est el tiempo de respuesta, que es el tiempo total
desde que se enva un mensaje hasta que se recibe la respuesta. En ese caso se incluye
tambin el tiempo de procesamiento del mensaje en el servidor, adems de la latencia
de ida y vuelta. Un juego con restricciones importantes debera evitar depender de
informacin que se obtiene como respuesta a una peticin directa. Es frecuente utilizar
mensajes con semntica oneway, es decir, que no implican esperar una respuesta en
ese punto.
An hay otra consideracin muy importante relacionada con la latencia: el jitter.
El jitter es la variacin de la latencia a lo largo del tiempo. Cuando se utilizan
redes basadas en conmutacin de paquetes con protocolos best-effort como IP
las condiciones puntuales de carga de la red en cada momento (principalmente
los encaminadores) afectan decisivamente al retardo de modo que puede cambiar
signicativamente entre un mensaje y el siguiente.
La latencia y el jitter permisible dependen mucho del tipo de juego y estn muy
inuenciados por factores psicomotrices, de percepcin visual del espacio, etc. Los
juegos ms exigentes son los que requieren control continuo, como la mayora de
los shotters, simuladores de vuelo, carreras de coches, etc., que idealmente deberan
tener latencias menores a 100 ms. El punto de vista subjetivo aumenta la sensacin de
realismo y provoca que el jugador sea mucho ms sensible, afectando a la jugabilidad.
En otros tipos de juegos como los de estrategia o que dependen de decisiones
colaborativas, el retardo admisible puede ser de hasta 500 ms o incluso mayor.
[945]
La evolucin de los modelos de comunicaciones en los videojuegos en red y
la mayora de las tcnicas que veremos a continuacin tienen un nico objetivo:
compensar los efectos que la latencia tienen en la experiencia del usuario.
29.5.
Distribucin de informacin
Existen muchas tcnicas para reducir el impacto de las limitaciones del ancho
de banda y la latencia en las comunicaciones. Por ejemplo, para reducir el ancho de
banda necesario se pueden comprimir los mensajes, aunque eso aumenta la latencia y
el consumo de CPU tanto en el emisor como en el receptor.
Tambin se puede mejorar la eciencia agrupando mensajes (message batching).
La mayor parte de los mensajes que se intercambian los componentes de un juego
son bastante pequeos (apenas unos cientos de bytes). Utilizar mensajes mayores
(cercanos a la MTU (Maximum Transfer Unit)) normalmente es ms eciente, aunque
de nuevo eso puede afectar negativamente a la latencia. El primer mensaje de cada
bloque tendr que esperar a que la cantidad de informacin sea suciente antes de salir
del computador. Obviamente eso puede ser prohibitivo dependiendo de la informacin
de que se trate. La posicin del jugador debera noticarse lo antes posible, mientras
que su puntuacin actual puede esperar varios segundos.
Como en tantas otras situaciones, las tcnicas que permiten mejorar un aspecto a
menudo son perjudiciales para otros. Por ese motivo no se pueden dar pautas generales
que sean aplicables en todos los casos. En cada juego, y para cada ujo de informacin
habrn de analizarse las opciones disponibles.
Una de las tcnicas ms tiles consiste en modelar el estado de los objetos en
particular los jugadores y el mundo virtual de modo que los cambios puedan ser
noticados de forma asncrona, es decir, limitar al mnimo las situaciones en las que
la progresin del juego dependa de una respuesta inmediata a una peticin concreta
o de un evento asncrono (como el protocolo lockstep). Como se ha visto en otros
captulos, la mayora de los juegos se pueden modelar como un sistema dirigido por
eventos y no es diferente en el caso de la interaccin con componentes remotos.
Una peticin asncrona es aquella que no bloquea al programa, habitualmente
al cliente. Eso se puede conseguir realizando la peticin en un hilo de ejecucin
adicional y especicando un callback, permitiendo as progresar al juego. Cuando
llega la respuesta, se trata como un evento asncrono ms.
Otra posibilidad interesante siguiendo la misma idea es utilizar comunicaciones oneway, es decir, enviar mensajes que anuncian cambios (hacia o desde el servidor) pero que no implican una respuesta de vuelta a la fuente del mensaje. Los mensajes oneway (que podemos llamar eventos) permiten disear estrategias mucho ms
escalables. Una comunicacin basada en intercambio de mensajes peticinrespuesta
solo se puede implementar con un sistema de entrega unicast (un nico destino por
mensaje) mientras que una comunicacin basada en eventos puede aprovechar los mecanismos de difusin multicast de TCP/IP y otros protocolos.
C29
[946]
29.6.
Modelo de informacin
En un juego en red hay mucha informacin esttica (como el mapa del nivel
en un shotter), pero tambin muchos datos que se actualizan constantemente. La
responsabilidad de almacenar, actualizar y transmitir toda esa informacin lleva a
implementar diferentes modelos, que obviamente estn muy relacionados con los
modelos de comunicaciones que vimos en la seccin anterior:
Centralizado Es el modelo ms sencillo. Un nico componente (el servidor) recoge
la informacin procedente de los jugadores, actualiza el estado global del
juego, comprueba que es completo y correcto aplicando las reglas del juego
y otras restricciones y distribuye el resultado a los jugadores. Esto incluye
evaluar la interaccin entre los jugadores, detectar errores o trucos ilegales
(cheats) o actualizar las puntuaciones. Habr informacin que interese a todos
los jugadores por igual y deba ser transmitida a todos ellos, pero habr ciertos
datos que pueden implicar nicamente a algunos de los jugadores. Por ejemplo,
la orientacin y ngulo exacto del arma de cada jugador solo es importante para
aquellos que pueden verlo o ser alcanzados si dispara. Este tipo de anlisis puede
ahorrar muchos mensajes innecesarios. En resumen, en el modelo centralizado
nicamente el servidor conoce el mundo completo y cada jugador solo tiene la
informacin que le atae.
Replicado En este caso, varios (incluso todos) los participantes en el juego tienen
una copia del mundo virtual. Eso implica la necesidad de sincronizar esa
informacin entre los participantes. Por eso, resulta conveniente cuando la
mayor parte del estado del juego puede ser determinada de forma aislada, es
decir, con poca interaccin con los participantes.
Distribuido Cada participante tiene una parte del estado global del juego, normalmente la que ms le afecta, de modo que se reducen los mensajes necesarios
para determinar la situacin de cada jugador. El modelo distribuido es adecuado
cuando el estado depende mucho de variables difciles de prever. Lgicamente,
es menos adecuado para evitar inconsistencias.
29.7.
El uso que cualquier aplicacin hace de los recursos de red se puede cuanticar en
funcin de ciertas variables objetivas [90]:
El nmero de mensajes transmitidos.
La cantidad media de destinatarios por mensaje.
El ancho de banda medio requerido para enviar un mensaje a cada destinatario.
La urgencia de que el mensaje llegue a cada destinatario.
La cantidad de procesamiento necesario para procesar cada mensaje entrante.
Estos valores pueden ser medidos en una situacin de funcionamiento ptimo
o bien pueden ser calculados a partir de un modelo terico del juego. Tambin es
posible determinar cmo afectarn a estas variables los posibles cambios respecto a
una situacin de partida. Por ejemplo, en qu medida crece la cantidad o el tamao de
los mensajes en funcin del nmero de jugadores, el tamao del mapa, la latencia del
jugador ms lento, etc.
[947]
29.8.
Consistencia e inmediatez
Eco local
Un ejemplo cotidiano de la relacin
entre consistencia e inmediatez lo
tenemos en las aplicaciones de shell
o escritorio remoto. En estas aplicaciones suele existir la opcin de
utilizar eco local o eco remoto, es
decir, si la aparicin de las pulsaciones o posicin del ratn depende
directamente de nuestras acciones o
de la realimentacin que d el servidor.
Una mejora obvia es conseguir que el cliente calcule los cambios en el estado
que estima corresponden a la interaccin con el jugador previendo lo que ms tarde
anunciar el servidor. Obviamente, el servidor dispone de la informacin de todos los
participantes mientras que el participante solo dispone de la interaccin con el jugador
y la historia pasada.
Veamos un ejemplo. El jugador presiona el botn de avance, la aplicacin calcula
la nueva posicin del personaje y la representa. Al mismo tiempo un mensaje que
incluye esa accin se ha enviado al servidor. El servidor realiza el mismo clculo y
enva a todos los clientes involucrados la nueva posicin del personaje. En condiciones
normales, habr relativamente poca variacin entre la posicin calculada por el cliente
y por el servidor y no ser necesario que el cliente tome ninguna accin adicional.
Pero puede presentarse otra situacin. En el preciso momento en el que el jugador
avanzaba el personaje recibe un disparo. El servidor descarta el mensaje con la accin
de avance y notica la muerte de ese personaje. Todos los participantes han visto morir
al personaje mientras que el jugador ha sido testigo de la inconsistencia, primero lo
vio avanzar normalmente y luego de pronto aparecer muerto unos metros atrs.
Es una mejora obvia pero compleja de implementar. Para que el cliente pueda
determinar el estado previsible de personaje necesita aplicar todas las restricciones
fsicas del escenario. En el caso de un juego 3D eso implica colisiones, gravedad,
inercia, ..., cosas que no eran necesarias con un modelo cliente-servidor puro.
C29
vinculacin multisensorial
[948]
29.9.
Predicciones y extrapolacin
29.9.1.
Veamos en detalle el algoritmo que aplica el cliente para predecir cmo las
acciones del usuario afectan al estado del juego:
1. Procesar eventos procedentes de los controles (teclado, ratn, joystick, etc.).
2. Serializar las acciones del usuario y enviarlas al servidor.
3. Determinar qu elementos del mundo virtual sern inuidos con la accin del
usuario y calcular las modicaciones en su estado.
4. Renderizar la nueva situacin del juego.
5. Esperar las actualizaciones de estado del servidor.
6. Si hay diferencias, modicar el estado del juego para compensar los errores
cometidos.
Aunque el usuario tiene realimentacin inmediata de sus acciones, el tiempo
transcurrido entre el paso 4 y el 6 (el tiempo de respuesta) sigue siendo determinante.
Si las predicciones no son rpidamente corroboradas o corregidas con la informacin
procedente del servidor, el error cometido por la prediccin se ir acumulando
aumentando las diferencia entre el estado real del juego (mantenido por el servidor) y
el estimado por el cliente.
Ntese que el cliente puede adems enviar la prediccin al servidor, de modo
que ste pueda validarla. As el servidor solo enviar datos si detecta que el error ha
superado determinado umbral. Mientras el cliente sea capaz de predecir correctamente
la trayectoria, el servidor simplemente tiene que dar el visto bueno, y no tiene porqu
hacerlo constantemente. Esto reduce el trco desde el servidor hacia los clientes,
pero le supone una carga de trabajo adicional.
[949]
El problema surge cuando el servidor enva correcciones respecto a la prediccin.
La correccin tendr una marca de tiempo en el pasado, de modo que el cliente debera
desechar todos las modicaciones que ha hecho sobre el estado el juego desde la marca
hasta el presente. Despus debe volver a aplicar todos los eventos que se han producido
desde ese instante para estimar el nuevo estado.
Eso signica que el estado previsto por el cliente que no han sido corroborada por
el servidor debe considerarse tentativo, y los eventos que se han producido desde el
ltimo instante vericado deben almacenarse temporalmente.
29.9.2.
Esta tcnica (tambin conocida como dead reckoning [10, 9.3]) se utiliza para
calcular el estado de entidades bajo el control de otros jugadores o del motor del
juego.
Para predecir la posicin futura de un objeto se asume que las caractersticas
de su movimiento (direccin y velocidad) se mantendrn durante el siguiente ciclo.
Como en la prediccin del jugador, el cliente puede publicar sus predicciones. El
servidor (o los otros participantes si es una arquitectura distribuida) verica el error
cometido y decide si debe enviar la informacin real para realizar las correcciones.
Esta informacin puede incluir la posicin, direccin y sentido, velocidad e incluso
aceleracin. De ese modo, el cliente no necesita estimar o medir esos parmetros,
puede utilizar los que proporciona la fuente, sea otro jugador o el propio motor del
juego, ahorrando clculos innecesarios. La gura 29.3 ilustra este proceso.
Figura 29.3: Cuando la prediccin calcula una posicin fuera del umbral permitido (fuera de la lnea roja),
el servidor enva una actualizacin (marcas azules) que incluye la posicin, direccin y velocidad real
actual. El cliente recalcula su prediccin con la nueva informacin
(29.1)
a(t1 t1 )2
2
(29.2)
C29
[950]
Figura 29.4: Cuando se reajusta la posicin se producen saltos (lneas discontinuas azules) mientras que
aplicando un algoritmo de convergencia (lneas verdes) el objeto sigue una trayectoria continua a pesar de
no seguir elmente la trayectoria real y tener que hacer ajustes de velocidad
Aunque es una tcnica muy efectiva para evitar que el usuario sea consciente de
los errores cometidos por el algoritmo de prediccin, pueden presentarse situaciones
ms complicadas. Si en un juego de carreras, la ruta predicha implica que un coche
sigue en lnea recta, pero en la real toma un desvo, la ruta de convergencia llevara
el coche a atravesar una zona sin asfaltar, que puede transgredir las propias reglas del
juego. Este escenario es muy comn en los receptores de GPS (Global Positioning
System) que utilizan una tcnica de prediccin muy similar (ver gura 29.5).
[951]
29.10.
Sockets TCP/IP
29.10.1.
Creacin de Sockets
Aunque, por norma general, los sockets desarrollados en los sistemas operativos no
son exactamente iguales, es cierto que casi todos los sistemas operativos han seguido
los denominados Berkeley Sockets como gua de diseo y por lo tanto, como API para
la programacin de aplicaciones en red. En esta seccin vamos a seguir los pasos de
programacin de un entorno GNU/Linux que es extensible a toda la familia UNIX.
Con matices mas amplios sirve para entender el interfaz de programacin socket en
general.
Veamos paso a paso la creacin de sockets, si vemos la primitiva para crear un
socket:
sockfd = socket(int socket_family, int socket_type, int protocol);
C29
[952]
[953]
Esta decisin, a groso modo est relacionada con si queremos una comunicacin
conable o no, es decir, en el caso de Internet, decidir entre usar un SOCK_STREAM
o SOCK_DGRAM respectivamente y por ende, en la mayora de los casos, entre TCP
o UDP.
El uso de SOCK_STREAM implica una conexin conable que lleva asociado
unos mecanismos de reenvo, ordenacin, etc. que consume tiempo de procesamiento a cambio de garantizar la entrega en el mismo orden de envo. En el caso de
SOCK_DGRAM no se proporciona conabilidad con lo que el tiempo de procesamiento se reduce.
Qu tipo de comunicacin empleo?, bien, en funcin del tipo de datos que estemos transmitiendo deberemos usar un mecanismo u otro. Como norma general todos
aquellos datos en tiempo real que no tiene sentido reenviar irn sobre SOCK_DGRAM
mientras que aquellos tipos de datos mas sensibles y que son imprescindibles para el
correcto funcionamiento deben ir sobre SOCK_STREAM.
Algunos ejemplos de uso de ambos protocolos nos pueden claricar qu usos se le
dan a uno y otro protocolo. En las ltimamente populares conexiones multimedia en
tiempo real, por ejemplo una comunicacin VoIP, se suele usar para la transmisin del
audio propiamente dicho el protocolo RTP (Real Time Protocol) que proporciona una
conexin no conable. Efectivamente, no tiene sentido reenviar una porcin de audio
que se pierde ya que no se puede reproducir fuera de orden y almacenar todo el audio
hasta que el reenvo se produce generalmente no es aceptable en las comunicaciones.
No obstante, en esas mismas comunicaciones el control de la sesin se delega en
el protocolo (SIP (Session Initiation Protocol)) encargado del establecimiento de la
llamada. Este tipo de datos requieren de conabilidad y por ello SIP se implementa
sobre TCP.
Podemos trasladar este tipo de decisiones al diseo de los mdulos de networking
de un determinado videojuego. Una vez decidida la informacin necesaria que se debe
transmitir entre los diversos participantes, debemos clasicar y decidir, para cada ujo
de informacin si va a ser mediante una conexin conable no conable.
Es difcil dar normas genricas por que, como adelantamos en la introduccin,
cada videojuego es diferente, no obstante, generalmente se utilizar comunicaciones
no conables para:
Aquella informacin que por su naturaleza, lleven el tiempo implcito en su
validez como informacin. Generalmente toda la informacin de tiempo real
como movimientos de granularidad na , audio y vdeo, etc.
Informacin que pueda ser inferida a partir de datos que recibimos antes
o despus. Por ejemplo, si el movimiento de un personaje se compone de
varios movimientos intermedios y se puede inferir uno de esos movimiento
intermedios a partir del resto (o incluso ignorar), esa informacin es candidata
a ser transmitida mediante una conexin o conable.
El resto de informacin que no cae en estas dos secciones puede ser susceptible de
implementarse utilizando comunicaciones conables.
Este primer paso de decisin de qu tipo de comunicacin se necesita es
importante ya que determina la estructura del cliente y el servidor.
C29
[954]
29.10.2.
Comunicaciones conables
Las comunicaciones conables garantizan la llegada de la informacin transmitida, sin prdidas y la entrega al destino en el orden en el cual fueron enviados. La
conguracin por defecto de los sockets utiliza TCP y la estructura asociada a los
clientes tiene la siguientes primitivas:
struct hostent *gethostbyname(const char *name): Esta estructura nos sirve para
identicar al servidor donde nos vamos a conectar y puede ser un nombre (e.j.
ejemplo.nombre.es), direccin IPv4 (e.j. 161.67.27.1) o IPv6. En el caso de
error devuelve null.
int connect(int sockfd, const struct sockaddr
*serv_addr, socklen_t addrlen): Esta primitiva nos permite realizar la conexin
con un servidor especicado por serv_addr usando el socket sockfd. Devuelve
0 si se ha tenido xito.
ssize_t
send(int s, const void *msg, size_t len, int ags): esta primitiva es utilizada para
mandar mensajes a travs de un socket indicando, el mensaje (msg), la longitud
del mismo (len) y con unas opciones denidas en ags. Devuelve el nmero de
caracteres enviados.
ssize_t
recv(int s, void *buf, size_t lon, int ags): esta primitiva es utilizada para recibir
mensajes a travs de un socket ((s)), el mensaje es guardado en buf, se leen los
bytes especicados por lon y con unas opciones denidas en ags. Devuelve el
nmero de caracteres recibidos.
close(int sockfd): cierra un socket liberando los recursos asociados.
Con estas primitivas podemos escribir un cliente que se conecta a cualquier servidor e interacciona con l. Para la implementacin del servidor tenemos las mismas
primitivas de creacin, envo, recepcin y liberacin. No obstante, necesitamos algunas primitivas mas:
int bind(int sockfd, struct sockaddr *my_addr,
socklen_t addrlen): Una vez creado un socket que nos va a servir de servidor
necesitamos asociarle una direccin local (my_addr y su longitud addrlen).
int listen(int s, int backlog): Con esta primitiva se indica que el socket s va a
aceptar conexiones entrantes hasta un lmite de conexiones pendientes denido
en backlog.
int accept(int s, struct sockaddr *addr,
socklen_t *addrlen): Esta funcin acepta una conexin pendiente, le asocia
un nuevo socket que ser devuelto por la funcin y que se emplear para la
transmisin y recepcin de datos propiamente dicho con el cliente que se acaba
de aceptar. Llamaremos a este nuevo socket, socket de servicio.
Veamos una implementacin de ejemplo que nos sirva para inscribir a un personaje
en una partida gestionada por un servidor. En este primer ejemplo un usuario se
desea registrar en un servidor de cara a enrolarse en una partida multijugador. La
informacin de usuario ser expresada por una estructura en C que podemos ver en el
listado 29.1 conteniendo el nick del jugador, su tipo de cuenta y su clave. Este tipo
de informacin es necesario transmitirlo por una conexin conable ya que queremos
que el proceso se realice de forma conable.
[955]
Listado 29.1: Estructura de registro de jugadores
1 struct player{
2
char nick[10];
3
int account_type;
4
char passwd[4];
5 };
C29
[956]
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 }
Una vez creado y vinculado un socket a una direccin, estamos listos para entrar
en un bucle de servicio donde, se aceptan conexiones, se produce el intercambio de
mensajes con el cliente conectado y con posterioridad se cierra el socket de servicio y
se vuelven a aceptar conexiones. Si mientras se atiende a un cliente se intenta conectar
otro, queda a la espera hasta un mximo especicado con la funcin listen. Si se supera
ese mximo se rechazan las conexiones. En este caso la interaccin se limita a leer los
datos de subscripcin del cliente.
El cliente, tal y como vemos en el listado 29.3, sigue unos pasos similares en
cuanto a la creacin del socket e inicializacin de las direcciones. En este caso se debe
introducir la direccin IP y puerto del servidor al cual nos queremos conectar. En este
caso localhost indica que van a estar en la misma mquina (linea 19). A continuacin
nos conectamos y enviamos la informacin de subscripcin.
En este ejemplo, el diseo del protocolo implementado no podra ser mas
simple, la sintaxis de los paquetes estn denidos por la estructura, la semntica es
relativamente simple ya que el nico paquete que se enva indican los datos de registro
mientras que la temporizacin tambin es muy sencilla al ser un nico paquete.
Listado 29.3: Cliente que registra un usuario en el servidor
1 int main(int argc, char *argv[])
2 {
3
4
struct sockaddr_in serverAddr;
5
struct hostent
*server_name;
6
int serviceSocket;
7
unsigned int serverPort;
8
struct player myData;
9
int send_bytes=0;
10
int len_server=0;
11
12
strncpy(myData.nick,"Carpanta",sizeof("Carpanta"));
13
myData.account_type = PREMIUM;
14
strncpy(myData.passwd,"123",sizeof("123"));
15
16
serverPort = PORT;
17
serviceSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
18
19
server_name = gethostbyname("localhost");
20
memset(&serverAddr, 0, sizeof(serverAddr));
21
memcpy(&serverAddr.sin_addr, server_name->h_addr_list[0],
server_name->h_length);
[957]
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(serverPort);
22
23
24
25
26
27
28
29
30
31
32
33 }
len_server = sizeof(serverAddr);
printf("Connecting to server..");
connect(serviceSocket,(struct sockaddr *) &serverAddr,(
socklen_t)len_server);
printf("Done!\n Sending credentials.. %s\n", myData.nick);
send_bytes = send(serviceSocket, &myData, sizeof(myData),0);
printf("Done! %d\n",send_bytes);
close(serviceSocket);
return 0;
29.10.3.
La informacin de estado que un
servidor le enva a un cliente es slo
aquella informacin relativa a su
visin parcial del estado del juego.
Es decir, la mnima imprescindible
para que el cliente pueda tener la
informacin de qu le puede afectar
en cada momento
El servidor (listado 29.5) en este caso prepara una estructura que representa la
posicin de un jugador (lineas 10-14), que podra ser un jugador manejado por la
IA del juego, a continuacin se crea el socket de servicio y se prepara para aceptar
paquetes de cualquier cliente en el puerto 3000. Finalmente se vincula el socket a
la direccin mediante la operacin bind y se pone a la espera de recibir mensajes
mediante la operacin recvfrom. Una vez recibido un mensaje, en la direccin
clientAddr tenemos la direccin del cliente, que podemos utilizar para enviarle
mensajes (en este caso mediante la operacin sendto).
C29
Informacin de estado
Comunicaciones no conables
[958]
localAddr) );
recvfrom(serviceSocket, &player, sizeof(struct position), 0, (
struct sockaddr *)&clientAddr, &length );
printf("Player ID: %d is in position %f, %f, %f with orientation %f"
,player.userID,player.x, player.y, player.z, player.rotation
[0][0]);
sendto(serviceSocket, &otherplayer, sizeof(struct position), 0, (
struct sockaddr *)&clientAddr, length );
return 0;
El cliente en este caso guarda muchas similitudes con el servidor (listado 29.6).
En primer lugar se prepara la estructura a enviar, se crea el socket, y se prepara la
direccin de destino, en este caso, la del servidor que se encuentra en el mismo host
(lineas 21-25), una vez realizada la vinculacin se enva la posicin y se queda a la
espera de paquetes que provienen del servidor mediante la funcin recvfrom.
Listado 29.6: Cliente de un jugador
1 int main(int argc, char *argv[])
2 {
3
int serviceSocket;
4
unsigned int length;
5
struct hostent *server_name;
6
struct sockaddr_in localAddr, serverAddr;
7
struct position mypos;
8
struct position enemy;
9
10
mypos.userID=10;
11
mypos.x=23;
12
mypos.y=21;
13
mypos.z=0;
14
mypos.rotation[0][0]=1;
15
16
serviceSocket = socket( AF_INET, SOCK_DGRAM, 0 );
17
localAddr.sin_family = AF_INET;
18
localAddr.sin_port = 0;
19
localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
20
21
server_name = gethostbyname("localhost");
22
memset(&serverAddr, 0, sizeof(serverAddr));
23
memcpy(&serverAddr.sin_addr, server_name->h_addr_list[0],
24
server_name->h_length);
serverAddr.sin_family = AF_INET;
[959]
25
26
27
28
29
30
31
32
33
34 }
serverAddr.sin_port = htons(3000);
memset( &( serverAddr.sin_zero), \0, 8 );
length = sizeof( struct sockaddr_in );
bind( serviceSocket, (struct sockaddr *)&localAddr, length );
sendto(serviceSocket, &mypos, sizeof(struct position), 0, (struct
sockaddr *)&serverAddr, length );
recvfrom(serviceSocket, &enemy, sizeof(struct position), 0, (
struct sockaddr *)&serverAddr, &length );
printf("My enemy ID: %d is in position %f, %f, %f with orientation %
f\n",enemy.userID,enemy.x, enemy.y, enemy.z, enemy.rotation
[0][0]);
return 0;
A lo largo de esta seccin hemos visto las funciones bsicas de envo y recepcin
mediante sockets TCP/IP. Aunque ya tenemos la base para construir cualquier tipo de
servidor y cliente, en la parte de servidores hace falta introducir algunas caractersticas
adicionales de cara al desarrollo de servidores ecientes y con la capacidad de atender
a varios clientes de forma simultnea.
29.11.
C29
[960]
La idea del select es que cuando retorna en cada uno de esos conjuntos se
encuentran los identicadores de los sockets de los cuales puedes leer, escribir o hay
alguna excepcin. Con las operaciones FD_ZERO, FD_SET, FD_CLR y FD_ISSET
se limpian, aaden y eliminan descriptores y se pregunta por un descriptor concreto
en cualquiera de los conjuntos respectivamente.
Supongamos que queremos atender distintos tipos de conexiones en varios sockets,
en principio no hay un orden de llegada de los paquetes y por lo tanto, no podemos
bloquearnos en uno slo de los sockets. La funcin select nos puede ayudar. Podemos
evolucionar nuestro servidor UDP desarrollado anteriormente para incluir la funcin
select (listado 29.7).
Listado 29.7: Utilizacin del select en monitorizacin de sockets
1
2
3
4
5
6
7
8
9
10
11
12
13
14 }
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(serviceSocket,&readSet);
while(select(serviceSocket+1, &readSet,0,0,NULL)){
if (FD_ISSET(serviceSocket,&readSet)){
recvfrom(serviceSocket, &player, sizeof(struct position), 0,
(struct sockaddr *)&clientAddr, &length );
printf("Player ID: %d is in position %f, %f, %f with orientation
%f",player.userID,player.x, player.y, player.z, player.
rotation[0][0]);
sendto(serviceSocket, &otherplayer, sizeof(struct position),
0, (struct sockaddr *)&clientAddr, length );
}
}
return 0;
[961]
Un aspecto a tener en cuenta con la funcin select es la semntica asociada a la
estructura timeval. Algunos sistemas operativas actualizan el valor almacenado en esa
estructura para reejar la cantidad de tiempo que quedaba antes del timeout asociado.
El problema es que otros sistemas operativos no actualizan este valor por lo que esta
semntica puede variar.
29.11.1.
Operaciones bloqueantes
De las funciones que estamos viendo, muchas de ellas son bloqueantes, es decir, se
invocan y duermen el hilo que la invoca hasta que algo relevante sucede. Por ejemplo,
el accept es una funcin bloqueante, recv, recvfrom, etc.
Mas que las funciones, la propiedad de que las operaciones sean o no bloqueantes
est asociada a la creacin del socket. Por defecto en la creacin de un socket, se
establece la propiedad de que las operaciones relacionadas con ese descriptor de socket
sean bloqueantes. Para cambiar esta propiedad, debemos hacer uso de la funcin int
fcntl(int fd, int cmd, long arg); que coge como argumento un descriptor de archivo
(fd), un comando y una serie de opciones. La funcin fcntl se dedica a los descriptores
de archivo en general, para establecer un socket no bloqueante hay que invocarla
con el identicador del socket, FS_SETFL para el comando y O_NONBLOCK como
argumento.
Las operaciones realizadas sobre el socket no sern bloqueantes por lo que habr
que gestionar apropiadamente esta condicin y consultar el estado de los sockets de
forma continua. Esta conguracin es muy til, por ejemplo, para hacer peridico
el acceso de las comunicaciones hacindolas mas predecibles y mientras seguir
realizando otras tareas.
Hay que tener en cuenta que, en entornos Linux, las operaciones bloqueantes
duermen el hilo que las invoca mientras no pase algo asociado a la funcionalidad de
la funcin bloqueante (por ejemplo, accept, recv, etc.). Si optamos por operaciones no
bloqueantes y por ello nos vemos obligados a realizar continuas consultas, deberemos
gestionar nosotros mismos nuestros periodos dormidos si no queremos consumir
recursos de CPU de forma innecesaria.
De igual manera, el valor devuelto por las operaciones realizadas sobre socket
no bloqueantes generalmente ser un error si no hay nada interesante asociado (por
ejemplo, no hay datos que leer o conexin que aceptar). El valor al cual se asocia
errno es EWOULDBLOCK (no obstante, en funcin de la implementacin y sistema
operativo utilizado debe consultarse este valor).
29.11.2.
Gestin de buffers
Los buffers son estructuras que se utilizan para almacenar informacin de forma
temporal para su procesado. Son estructuras necesarias para muchos cometidos como
puede ser asociar procesos que tienen distintas velocidades en el procesamiento de la
informacin, almacenamiento temporal, etc.
C29
Cuando realizamos operaciones con sockets, los datos pasan realmente por una
sucesin de buffers en el kernel que permiten el tratamiento de ujos continuos de
bytes mediante operaciones parciales. Por lo tanto cuando recibimos informacin de
un socket puede que no se nos proporcione toda la informacin recibida al igual que
cuando enviamos una informacin puede que no se mande de forma completa en la
misma operacin. Esta gestin de cuando procesar la informacin tiene que ver con
decisiones del kernel.
[962]
En el caso de send, tenemos la operacin sendall que, con la misma semntica que
el send, nos permite enviar todo un buffer.
Existen otros casos en el cual la gestin de buffers puede ralentizar las prestaciones
ya que se necesitan copiar de forma continua. Por ejemplo si leemos de un archivo, la
operacin read sobre un descriptor de archivo almacena la informacin en un buffer
interno al kernel que es copiado al buffer en espacio de usuario. Si a continuacin
queremos enviar esa informacin por un sockets, invocaramos la funcin send y
esa funcin copiara ese buffer, de nuevo a un buffer interno para su procesado.
Este continuo trasiego de informacin ralentiza la operacin si lo que queremos es
transmitir por completo el archivo. Lo ideal sera que las dos operaciones entre los
buffers internos al buffer de aplicacin se convirtiera en una nica operacin entre
buffers internos.
La tcnica que minimiza las copias innecesarias entre buffers se denomina zerocopy y, a menudo, puede obtener una considerable mejora de las prestaciones. Es
difcil estudiar esta tcnica en profundidad porque depende del sistema operativo y de
la aplicacin concreta.
29.11.3.
Serializacin de datos
La amplia variedad de sistemas y conguraciones posibles hacen que la representacin de datos y portabilidad de cualquier programa sea un aspecto importante a
tener en cuenta en el desarrollo de sistemas multiplataforma. La programacin con
sockets no escapa a este problema y, al ser una programacin de bajo nivel, tenemos
que ocuparnos de aspectos como la representacin de datos binarios en distintas plataformas y como transmitir este tipo de datos. A este nivel la principal distincin es el
ordenamiento de los bytes (little-endian y big-endian).
En los listados anteriores, esta caracterstica la omitamos a excepcin de la
utilizacin de las funciones htons() y ntohs() que utilizamos para pasar a una
codicacin comn denominada Network Byte Order. Estas funciones nos permiten
lidiar con un formato comn en muchas plataformas y las podemos utilizar para la
gestin de enteros y otantes:
1
2
3
4
5
#include
uint32_t
uint16_t
uint32_t
uint16_t
<arpa/inet.h>
htonl(uint32_t
htons(uint16_t
ntohl(uint32_t
ntohs(uint16_t
hostlong);
hostshort);
netlong);
netshort);
[963]
A mas alto nivel se puede usar XDR (eXternal Data Representation), un estndar
para la descripcin y representacin de datos utilizado por ONC (Open Network
Computing)-RPC (Remote Procedure Call) pero el cual se puede usar de forma directa
mediante las libreras adecuadas. Como veremos mas adelante, una de las principales
labores que nos ahorra el uso de middlewares es precisamente, la gestin de la
representacin de datos.
29.11.4.
Multicast y Broadcast
Los modelos de comunicacin vistos hasta ahora estn asociados a una comunicacin cliente-servidor uno a uno. Este tipo de comunicaciones entre pares iguales se
denomina unicast. Existen otros paradigmas de comunicacin como son las comunicaciones uno a muchos (Multicast) y uno a todos (Broadcast). En el caso de comunicaciones Multicast, el servidor comunica a un grupo de clientes identicados por un
identicador (en IP existen un grupo de direcciones especiales para identicar estos
grupos) de forma comn algn dato de su inters. Este tipo de comunicacin es mucho
mas efectiva ya que substituye, para un grupo de n clientes, a n conexiones unicast.
En el caso de las comunicaciones Broadcast la comunicacin se establece dentro de
una subred entre el servidor y todos los computadores de ese dominio. Por motivos
obvios las comunicaciones broadcast nunca atraviesan un router.
Para implementar la comunicacin multicast se debe subscribir el cliente y el
servidor a una direccin de grupo comn. El rango de direcciones IPv4 para grupos
multicast va desde 224.0.0.0 hasta la 239.255.255.255. Es necesario consultar la
pgina web de la IANA (Internet Assigned Numbers Authority) ya que muchas de
esas direcciones estn asignadas a grupos preestablecidos.
El envo de un mensaje a un grupo multicast no se diferencia, en el cdigo fuente,
salvo por la direccin a la cual se enva que pertenece al rango anteriormente descrito.
En la parte del receptor hay ligeras variaciones con dos objetivos principales:
Abrir un socket que pertenece a un grupo multicast.
Indicarle al sistema operativo que se una al grupo para recibir sus mensajes.
Para abrir un socket que pertenece a un grupo multicast:
1 setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&argument,sizeof(argument))
en esta llamada a setsockopt (que nos ayuda a establecer las opciones de los socket)
donde bsicamente se especica que para una direccin multicast, vamos a reutilizar
la direccin compartiendo varios socket el mismo puerto. En este caso argument es un
u_int con un valor de 1. Faltara la segunda parte:
5
6
7
C29
1
2
3
4
[964]
29.11.5.
29.12.
[965]
Middlewares de comunicaciones
podrn ser invocadas remotamente por un cliente. Los dems, y la mayora de los
actuales, son RMI (Remote Method Invocation), es decir, son middlewares orientados
a objetos. La gura 29.6 muestra el esquema de invocacin remota a travs de un
ncleo de comunicaciones tpico de este tipo de middlewares.
C29
Objeto distribuido
[966]
29.12.1.
ZeroC Ice
iOS, etc.) lo que proporciona una gran exibilidad para construir sistemas muy
heterogneos o integrar sistemas existentes.
29.12.2.
Especicacin de interfaces
Cuando nos planteamos una interaccin con un objeto remoto, lo primero es denir
el contrato, es decir, el protocolo concreto que cliente y objeto (servidor) van a
utilizar para comunicarse.
Antes de las RPC cada nueva aplicacin implicaba denir un nuevo protocolo
(independientemente si es pblico o no) y programar las rutinas de serializacin y
des-serializacin de los parmetros de los mensajes para convertirlos en secuencias
de bytes, que es lo que realmente podemos enviar a travs de los sockets. Esta tarea
puede ser compleja porque se tiene que concretar la ordenacin de bytes, el padding
para datos estructurados, las diferencias entre arquitecturas, etc.
Los middleware permiten especicar la interfaz mediante un lenguaje de programacin de estilo declarativo. A partir de dicha especicacin, un compilador genera
cdigo que encapsula toda la lgica necesaria para (des)serializar los mensajes especcos produciendo una representacin externa cannica de los datos. A menudo el
compilador tambin genera esqueletos para el cdigo dependiente del problema. El
ingeniero nicamente tiene que rellenar la implementacin de los funciones o mtodos.
El lenguaje de especicacin de interfaces de Ice se llama S LICE (Specication
Language for Ice) y proporciona compiladores de interfaces (translators) para todos
los lenguajes soportados dado que el cdigo generado tendr que compilar/enlazar con
el cdigo que aporte el programador de la aplicacin. En cierto sentido, el compilador
de interfaces es un generador de protocolos a medida para nuestra aplicacin.
29.12.3.
Terminologa
S LICE
El lenguaje S LICE (Specication
Language for Ice) al igual que otros
muchos lenguajes para denicin
de interfaces (como IDL (Interface
Denition Language) o XDR) son
puramente declarativos, es decir, no
permiten especicar lgica ni funcionalidad, nicamente interfaces.
[967]
Sirviente
Es el cdigo concreto que respalda la funcionalidad del objeto, es quin
realmente hace el trabajo.
Cliente
Es la entidad activa, la que solicita servicios de un objeto remoto mediante
invocaciones a sus mtodos.
Proxy
Es un representante de un objeto remoto. El cliente en realidad interacciona (invoca) a un proxy, y ste se encarga con la ayuda del ncleo de comunicaciones,
de llevar el mensaje hasta el objeto remoto y despus devolverle la respuesta al
cliente.
proxy
Ice API
aplicacin servidora
red
Ice API
esqueleto
adaptador
de objeto
C29
[968]
29.12.4.
Esto genera dos cheros llamados Hello.cpp y Hello.h que deben ser
compilador para obtener las aplicaciones cliente y servidor.
[969]
Sirviente
El compilador de interfaces ha generado una clase Example::Hello. La implementacin del sirviente debe heredar de esa interfaz proporcionando una implementacin
(por sobrecarga) de los mtodos especicados en la interfaz S LICE.
El propio compilador de interfaces puede generar una clase hueca que sirva al
programador como punto de partida para implementar el sirviente:
$ slice2cpp --impl Hello.ice
#ifndef __HelloI_h__
#define __HelloI_h__
#include <Hello.h>
namespace Example {
#endif
#include <iostream>
#include "HelloI.h"
void
Example::HelloI::puts(const ::std::string& message,
const Ice::Current& current) {
std::cout << message << std::endl;
}
C29
3 El
[970]
#include <Ice/Ice.h>
#include "HelloI.h"
using namespace std;
using namespace Ice;
class Server: public Application {
int run(int argc, char* argv[]) {
Example::HelloPtr servant = new Example::HelloI();
ObjectAdapterPtr adapter = \
communicator()->createObjectAdapter("HelloAdapter");
ObjectPrx proxy = adapter->add(
servant, communicator()->stringToIdentity("hello1"));
cout << communicator()->proxyToString(proxy) << endl;
adapter->activate();
shutdownOnInterrupt();
communicator()->waitForShutdown();
return 0;
}
};
int main(int argc, char* argv[]) {
Server app;
return app.main(argc, argv);
}
[971]
Cliente
La aplicacin cliente nicamente debe conseguir una referencia al objeto remoto
e invocar el mtodo puts(). El cliente tambin se puede implementar como una
especializacin de la clase Ice::Application. El cdigo completo del cliente aparece a
continuacin:
Listado 29.12: Cliente de la aplicacin Hello: Cliente.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <Ice/Ice.h>
#include "Hello.h"
using namespace Ice;
using namespace Example;
class Client: public Ice::Application {
int run(int argc, char* argv[]) {
ObjectPrx proxy = communicator()->stringToProxy(argv[1]);
HelloPrx hello = HelloPrx::checkedCast(proxy);
hello->puts("Hello, World!");
}
};
return 0;
El programa acepta por lnea de comandos la representacin textual del proxy del
objeto remoto. A partir de ella se obtiene un objeto proxy (lnea 8). Sin embargo esa
referencia es para un proxy genrico. Para poder invocar los mtodos de la interfaz
Hello se requiere una referencia de tipo HelloPrx, es decir, un proxy a un objeto
remoto Hello.
Para lograrlo debemos realizar un moldeado del puntero (downcasting4 ) mediante
el mtodo esttico HelloPrx::checkedCast() (lnea 9). Gracias al soporte de
introspeccin de los objetos remotos, Ice puede comprobar si efectivamente el objeto
remoto es del tipo al que tratamos de convertirlo. Esta comprobacin no se realizara
si empleramos la modalidad uncheckedCast().
Una vez conseguido el proxy del tipo correcto (objeto hello) podemos invocar
el mtodo remoto puts() (lnea 12) pasando por parmetro la cadena "Hello,
World!".
Compilacin
La gura 29.9 muestra el esquema de compilacin del cliente y servidor a partir del
chero de especicacin de la interfaz. El cheros marcados en amarillo corresponden
a aquellos que el programador debe escribir o completar, mientras que los cheros
generados aparecen en naranja.
Para automatizar el proceso de compilacin utilizamos un chero Makefile, que
se muestra en el listado 29.13
C29
4 Consiste en moldear un puntero o referencia de una clase a una de sus subclases asumiendo que
realmente es una instancia de ese tipo.
[972]
Hello.ice
slice2cpp
translator
Hello.h
Hello.cpp
Client.cpp
Cliente
Figura 29.9: Esquema de compilacin de cliente y servidor
CC=g++
CXXFLAGS=-I.
LDLIBS=-lIce -lIceUtil
APP=Hello
STUBS=$(addprefix $(APP), .h .cpp)
all: Server Client
Server: Server.o $(APP)I.o $(APP).o
Client: Client.o $(APP).o
Server.cpp Client.cpp: $(STUBS)
%.cpp %.h: %.ice
slice2cpp $<
clean:
$(RM) Server Client $(STUBS) *.o *~
$(RM) *.bz2 IcePatch2.sum
Ejecutando el servidor
Si ejecutamos el servidor obtendremos un error:
$ ./Server
!! 03/10/12 19:52:05.733 ./Server: error: ObjectAdapterI.cpp:915: Ice
::InitializationException:
initialization exception:
object adapter HelloAdapter requires configuration
[973]
C29
29.12.5.
[974]
La principal diferencia entre las invocaciones twoway con respecto a los otros tipos
es que el cliente recupera el control del ujo de ejecucin tan pronto como el mensaje
de invocacin es enviado, sin necesidad de esperar ningn tipo de conrmacin o
respuesta desde el servidor (ver gura 29.10).
29.12.6.
Invocacin asncrona
[975]
Este mecanismo lo incorpora I CE de forma transparente, de modo que el programador no se tiene que preocupar del manejo de los hilos ni el tratamiento de las respuestas. El programador puede realizar llamadas remotas a los mismos mtodos denidos
en la interfaz utilizando un procedimiento diferente que involucra un objeto que permite obtener el estado de la invocacin y, eventualmente, obtener el resultado de la
operacin. A este mecanismo es a lo que denominamos invocacin asncrona.
De hecho I CE proporciona varias modalidades para realizar invocaciones asncronas. La que vemos aqu es probablemente la ms limpia y se basa en la creacin de
objetos callback. Para realizar la invocacin remota, el cliente utiliza un mtodo del
proxy con el prejo begin que acepta un parmetro adicional: el callback object.
Esta invocacin retorna inmediatamente y el cliente puede seguir ejecutando otras
operaciones.
Cuando la invocacin remota se ha completado, el ncleo de ejecucin de la
parte del cliente invoca el mtodo indicado en el objeto callback pasado inicialmente,
proporcionando los resultados de la invocacin. En caso de error, invoca otro mtodo
en el que proporciona informacin sobre la excepcin asociada.
En el listado 29.15 se muestra el cdigo completo para el cliente que crea y
proporciona un callback segn la tcnica indicada.
Listado 29.15: Cliente AMI que recibe la respuesta mediante un callback
(cpp-ami/Client-callback.cpp)
#include <Ice/Ice.h>
#include <factorial.h>
using namespace std;
using namespace Ice;
using namespace Example;
class FactorialCB : public IceUtil::Shared {
public:
void response(const Ice::Long retval) {
cout << "Callback: Value is " << retval << endl;
}
void failure(const Exception& ex) {
cout << "Exception is: " << ex << endl;
}
};
class Client: public Ice::Application {
public:
int run(int argc, char* argv[]) {
ObjectPrx proxy = communicator()->stringToProxy(argv[1]);
MathPrx math = MathPrx::checkedCast(proxy);
Callback_Math_factorialPtr factorial_cb =
newCallback_Math_factorial(new FactorialCB,
&FactorialCB::response,
&FactorialCB::failure);
math->begin_factorial(atoi(argv[2]), factorial_cb);
return 0;
};
int main(int argc, char* argv[]) {
if (argc != 3) {
cerr << "usage: " << argv[0] << "<server> <value>" << endl;
return 1;
}
C29
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
[976]
42
43
Client app;
44
return app.main(argc, argv);
45 }
29.12.7.
Propagacin de eventos
#include
#include
#include
#include
<Ice/Application.h>
<IceStorm/IceStorm.h>
<IceUtil/UUID.h>
"Hello.h"
[977]
using namespace IceStorm;
using namespace Example;
class HelloI : public Hello {
void puts(const string& s, const Current& current) {
cout << "Event received: " << s << endl;
}
};
class Subscriber : public Application {
TopicManagerPrx get_topic_manager() {
string key = "IceStormAdmin.TopicManager.Default";
ObjectPrx base = communicator()->propertyToProxy(key);
if (!base) {
cerr << "property " << key << " not set." << endl;
return 0;
}
cout << "Using IceStorm in: " << key << " " << endl;
return TopicManagerPrx::checkedCast(base);
public:
virtual int run(int argc, char* argv[]) {
TopicManagerPrx topic_mgr = get_topic_manager();
if (!topic_mgr) {
cerr << appName() << ": invalid proxy" << endl;
return EXIT_FAILURE;
}
ObjectPtr servant = new HelloI;
ObjectAdapterPtr adapter = \
communicator()->createObjectAdapter("Hello.Subscriber");
ObjectPrx base = adapter->addWithUUID(servant);
TopicPrx topic;
try {
topic = topic_mgr->retrieve("HelloTopic");
topic->subscribeAndGetPublisher(QoS(), base);
}
catch(const NoSuchTopic& e) {
cerr << appName() << ": " << e
<< " name: " << e.name << endl;
return EXIT_FAILURE;
}
cout << "Waiting events... " << base << endl;
adapter->activate();
shutdownOnInterrupt();
communicator()->waitForShutdown();
topic->unsubscribe(base);
}
};
return EXIT_SUCCESS;
C29
[978]
Config=icestorm.config
2 IceBox.ServiceManager.Endpoints=tcp -p 7000
[979]
Slo se denen dos propiedades: La carga del servicio IceStorm y los endpoints
del gestor remoto de IceBox. La conguracin de IceStorm a la que se hace referencia
(icestorm.config) es la siguiente:
Listado 29.19: Conguracin de IceStorm
1 IceStorm.TopicManager.Endpoints=tcp -p 10000
2 IceStormAdmin.TopicManager.Default=IceStorm/TopicManager:tcp -p
10000
3 IceStorm.Publish.Endpoints=tcp -p 2000
4 Freeze.DbEnv.IceStorm.DbHome=db
5
6 Hello.Subscriber.Endpoints=tcp
Las lneas 13 especican los endpoints para los adaptadores del TopicManager,
el objeto de administracin y los publicadores de los canales. La lnea 4 es el nombre
del directorio donde se almacenar la conguracin del servicio (que es persistente
automticamente). La lnea 6 es el endpoint del adaptador del publicador, que se ha
incluido en este chero por simplicidad aunque no es parte de la conguracin de
IceStorm.
Una vez congurado podemos lanzar el servicio y probar el ejemplo. Lo primero
es arrancar el icebox (en background):
$ icebox --Ice.Config=icebox.config &
Este servicio puede resultar muy til para gestionar los eventos de actualizacin
en juegos distribuidos. Resulta sencillo crear canales (incluso por objeto o jugador)
para informar a los interesados de sus posicin o evolucin. El acoplamiento mnimo
que podemos conseguir entre los participantes resulta muy conveniente porque
permite que arranquen en cualquier orden y sin importar el nmero de ellos, aunque
obviamente una gran cantidad de participantes puede degradar el desempeo de la red.
C29
$ ./subscriber --Ice.Config=icestorm.config
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
Event received: Hello World!
[980]
Tambin resulta muy interesante que los eventos estn modelados como invocaciones. De ese modo es posible eliminar temporalmente el canal de eventos e invocar
directamente a un solo subscriptor durante las fases iniciales de modelado del protocolo y desarrollo de prototipos.
Captulo
30
Sonido y Multimedia
Guillermo Simmross Wattenberg
Francisco Jurado Monroy
n este captulo se discuten dos aspectos que son esenciales a la hora de afrontar
el desarrollo de un videojuego desde el punto de vista de la inmersin del
jugador. Uno de ellos est relacionado con la edicin de audio, mientras que
el otro est asociado a la integracin y gestin de escenas de vdeo dentro del propio
juego.
El apartado sonoro es un elemento fundamental para dotar de realismo a un
juego, y en l se aglutinan elementos esenciales como son los efectos sonoros y
elementos que acompaan a la evolucin del juego, como por ejemplo la banda
sonora. En este contexto, la primera parte de esta captulo describe la importancia
del audio en el proceso de desarrollo de videojuegos, haciendo especial hincapi en
las herramientas utilizadas, el proceso creativo, las tcnicas de creacin y aspectos
bsicos de programacin de audio.
Respecto a la integracin de vdeo, en la segunda parte del captulo se muestra la
problemtica existente y la importancia de integrar escenas de vdeo dentro del propio
juego. Dicha integracin contribuye a la evolucin del juego y fomenta la inmersin
del jugador en el mismo.
981
[982]
30.1.
Edicin de Audio
30.1.1.
Introduccin
La interactividad en un videojuego es la capacidad que este tiene de reaccionar a acciones realizadas por el jugador.
Proceso de inmersin
La msica es una herramienta nica
para hacer que el jugador se implique, viva y disfrute del videojuego.
[983]
Las acciones que se llevan a cabo al jugar a un videojuego pueden ser pulsaciones
de teclas, movimientos del ratn o del joystick, movimientos de un volante o de mando
inalmbrico y hoy en da hasta movimientos corporales del propio jugador. Todas estas
acciones pueden producir una reaccin en un videojuego.
Por todo lo anterior, la composicin musical y el diseo de sonido para videojuegos requiere de ciertas aptitudes complementarias sobre la creacin de audio general.
C30
[984]
Durante el proceso de desarrollo de un videojuego, es muy probable que evolucione en cierta manera la visin del videojuego que tengan el jefe de proyecto o el
director artstico y se vean obligados a replantear la ambientacin y aspecto de este, y por tanto las exigencias para la banda sonora o los efectos de sonido. Es una
situacin muy comn que piezas musicales que ya se haban aceptado como integrantes del videojuego tengan que ser recompuestas, adaptadas, recortadas, reorquestadas,
reinstrumentalizadas o en el peor de los casos descartadas para satisfacer las nuevas
caractersticas del juego o de un nuevo giro en su planteamiento.
[985]
Por todo lo anterior, el msico y diseador de sonido debe cumplir una serie
de aptitudes muy especiales para ser capaz de desarrollar tareas muy diversas, tanto
tcnicas como artsticas, para las que se necesitan conocimientos variados. Adems
de estas aptitudes, debe ser creativo, innovador, perseverante, trabajar rpido y ser
meticuloso.
30.1.2.
Secuenciad. multipistas
Figura 30.1: Para comenzar a crear un estudio casero slo necesitaremos un ordenador con una buena
tarjeta de sonido. Poco a poco podremos complementarlo con instrumentos reales, diferente hardware y
nuevo software.
C30
Muchos secuenciadores son tambin multipistas en los que se pueden crear pistas de audio adems de
pistas MIDI. Esto permite mezclar
instrumentos reales con instrumentos generados en el ordenador.
[986]
Software
La herramienta principal con la que contamos hoy da para la creacin de audio es
el software musical. Existen multitud de programas de audio en el mercado con muy
diferentes utilidades:
Secuenciadores: son programas capaces de grabar y reproducir una serie de
eventos MIDI, asignndoles diferentes muestras de sonido. Gracias a este tipo
de software podremos memorizar los eventos MIDI procedentes de diferentes
instrumentos. Ejemplos de este tipo de software son Linux Multimedia Studio
(LMMS), Rosegarden, Steinberg Cubase, Cakewalk Sonar, Avid Pro Tools,
Apple Logic o Propellerheads Reason.
Multipistas: permite reproducir y controlar diferentes instrumentos o sonidos
de forma independiente o conjunta, facilitando las labores de mezcla. Ejemplos
de este tipo de software son Ardour, Digidesign Pro Tools, Steinberg Cubase,
Cakewalk Sonar, Adobe Audition y Sony Vegas.
Editores: permiten grabar, manipular y procesar archivos de sonido provenientes de cheros en diferentes formatos y guardarlos en otros formatos. Las dos
caractersticas ms importantes de estos programas -la edicin y la conversin
a diferentes formatos- son esenciales en el mundo de la creacin de audio para
videojuegos. Ejemplos de este tipo de software son Audacity, Adobe Audition
o Sony Sound Forge.
Software de masterizacin: permiten retocar y ajustar un grupo de cheros
de sonido para que todos ellos estn igualados en volumen y funcionen como
un nico conjunto de piezas de audio. Aunque este tipo de software est ms
orientado a la produccin de discos musicales, puede ser tambin til para que,
una vez creado el material sonoro que se incluir en el videojuego, adaptemos
sus niveles y retoquemos por ltima vez la ecualizacin y compresin en caso
de ser necesario. Entre este tipo de programas podemos encontrar Sony CD
Architect, Steinberg Wavelab o T-RackS.
Plug-ins: son pequeas extensiones de la funcionalidad de un programa
antrin. Por lo general, en el mbito de la msica por ordenador, los plugins suelen ser mdulos de efectos para el procesado de sonido o instrumentos
virtuales. Hoy da existen todo tipo de plug-ins para aadir reverberacin, delay,
procesado dinmico, modulacin, efectos de guitarra, ltrado, etc y para simular
sintetizadores e instrumentos reales.
Software de loops: este tipo de software permite crear msica superponiendo
diferentes bloques que pueden repetirse una y otra vez. Estos programas resultan
tiles para crear bucles que sern incluidos en otras piezas musicales. Ejemplos
de este tipo de software son Sony Acid, Propellerhead Recycle, Ableton Live o
FL Studio.
Hardware
El apoyo necesario para facilitar la composicin y la conguracin del software
de audio puede conseguirse con diferentes tipos de hardware:
Instrumentos y controladores MIDI: el instrumento por excelencia para
dinamizar la comunicacin con el ordenador es el teclado. La casi totalidad
de los teclados actuales son capaces de comunicarse con otros dispositivos
mediante el protocolo MIDI. Gracias a l, la introduccin de notas y la
SW libre y audio
Existen multitud de alternativas
de software libre para la creacin de audio: LMMS (secuenciacin y software de loops), Ardour,
energyXT, Qtractor (secuenciacin,
multipistas y masterizacin), Audacity (edicin), mda plugin collection (plug-ins) y otros muchos.
[987]
composicin en general ser notablemente ms fcil y uida. Adems de otros
muchos tipos de instrumentos MIDI como bateras o guitarras, tambin pueden
encontrarse controladores de diferentes formas y utilidades: pads, mesas de
mezclas, trackballs, supercies...
Samplers: son dispositivos electrnicos que pueden almacenar y modicar
muestras de sonidos. El usuario puede cargar o grabar sonidos y crear tablas para
asignarlos a notas musicales y poder as utilizar un teclado, un secuenciador u
otro dispositivo para lanzar los sonidos almacenados.
Otros instrumentos: adems del teclado como instrumento central para componer msica por ordenador, la utilizacin de instrumentos clsicos, especialmente los que son difciles de emular digitalmente, como guitarras, vientos y
voz, dar a nuestra msica una nueva dimensin, aportando riqueza, humanidad
y realismo a las piezas musicales que compongamos, adems de abrirnos nuevas
posibilidades y estimular nuestra creatividad.
Figura 30.2: Existen multitud de controladores MIDI en el mercado. En la imagen, Native Instruments
Maschine (que es adems un sampler y un secuenciador).
30.1.3.
El proceso creativo
Actualmente las empresas desarrolladoras de videojuegos han realizado un esfuerzo considerable para adaptar la msica a las diferentes situaciones de los videojuegos,
creando tensin si algn suceso importante va a suceder en el desarrollo del juego o
tranquilizando el estilo musical si la accin que se desarrolla se corresponde con un
instante relajado de la historia.
C30
[988]
Preproduccin
El proceso creativo comienza con la fase de preproduccin, donde se comienza a
elaborar el documento de diseo del juego. Este documento detalla todos los aspectos
relevantes: historia, dilogos, mapas, sonido, grcos, animaciones y programacin.
En este momento se ja el tipo de msica y su papel en el videojuego y se elabora
una lista que dena la msica que tendr cada parte del juego, si habr un tema
principal, si la msica deber cambiar dependiendo de ciertas variables del videojuego
o cmo ciertos elementos musicales deben transmitir o corresponderse con detalles o
situaciones concretas.
Produccin
Una vez que el editor del juego aprueba el documento de diseo, el equipo de
desarrollo comienza su trabajo. La persona o personas encargadas de crear el audio de
un videojuego se incorpora al proyecto en las ltimas fases de desarrollo (al nalizar
la fase de produccin normalmente). Hasta este momento, lo ms probable es que el
equipo de programadores habr utilizado placeholders para los sonidos y su msica
favorita para acompaar al videojuego.
El creador de audio tiene que enfrentarse a la dura labor de crear sonidos y msica
lo sucientemente buenos como para conseguir que todo el equipo de desarrollo d
por buenos el nuevo material (aunque la decisin nal no sea del equipo de desarrollo,
pueden ejercer una gran inuencia sobre el veredicto nal).
En esta fase debe comprobarse tanto la validez tcnica de los sonidos y msica
(si se adaptan a las especicaciones) como la calidad del material entregado. Es
muy comn que se cambien, repitan o se aadan sonidos y/o composiciones en este
momento, por lo que el creador de audio debe estar preparado para estas contingencias
(previndolo en el presupuesto o creando a priori ms material del entregado).
Al nalizar el proceso de produccin, se determina cmo y en qu momentos
se reproducir qu archivo de sonido y con qu procesado en caso de utilizar
procesamiento en tiempo real. Por lo general, y pese a lo que pudiera parecer en
primera instancia, este proceso puede llevar hasta la mitad del tiempo dedicado a todo
el conjunto de la produccin.
[989]
dimensin a los videojuegos y otra variable con la que jugar durante el proceso de diseo: el sonido tridimensional puede convertirse en otro elemento protagonista para el
jugador, ya que puede permitirle posicionar cualquier fuente de sonido en un mundo
virtual (un enemigo, un objeto, etc.).
Post-produccin
Durante el proceso de post-produccin se lleva a cabo la masterizacin y mezcla
de los archivos de sonido una vez incluidos en el videojuego. Los sonidos y msica
que se reproducen durante una partida compiten simultneamente por ser odos por el
jugador. Es necesario tener en cuenta qu sonidos son ms o menos importantes en
cada momento del juego, y asignar un volumen adecuado a cada uno de ellos.
Al realizar la mezcla de los diferentes sonidos creados, debe tenerse en cuenta
el espectro de frecuencias de cada uno de los sonidos, para no acumular demasiados
sonidos en un mismo rango frecuencial. El proceso de mezclado tambin puede ser
dinmico y ser llevado a cabo automticamente por el motor de sonido. Los efectos
de sonidos pueden ser ecualizados o procesados en funcin de diferentes variables del
videojuego, pero tambin puede modicarse dinmicamente una partitura aadiendo
o eliminando instrumentos segn lo que suceda en el juego.
Una forma prctica de preparar el mezclado nal y evitar posibles problemas
es decidir desde un principio durante el proceso de pre-produccin incluso qu
frecuencias se asignarn a cada tipo de efecto de sonido, a la msica y a los dilogos.
Durante la produccin se tendr en cuenta esto para ya asignar bandas de frecuencias
durante el retoque de los sonidos, facilitando el posterior proceso de post-produccin.
El proceso de mezclado no consiste en aumentar el volumen de determinados
sonidos, sino de atenuar los menos importantes en cada momento. El problema es
decidir cul es la prioridad de la gran cantidad de sonidos que existen en un juego,
incluida msica y dilogos. Una vez ms, la naturaleza de los videojuegos hace
enormemente difcil la obtencin de una solucin ptima. La interactividad da un
grado de libertad al jugador que permite que coincidan varios sonidos o partes de la
msica importantes con dilogos clave para el desarrollo del juego. Esto obliga al
encargado de mezclar o de priorizar los sonidos a tomar la decisin menos mala o ms
ptima para los casos ms probables.
El proceso creativo de sonido en los videojuegos no solo se reere a la creacin
de audio, sino que una gran parte del tiempo se invierte en la implementacin y
acoplamiento de los sonidos y msica creada. Esta implementacin es especialmente
importante, puesto que determina cmo y cundo se reproducirn los sonidos durante
el videojuego. Adems, la complicacin que supone generar una mezcla de sonidos
adecuada hace an ms determinante esta parte del proceso creativo.
C30
Por tanto, no solo debemos tener en cuenta la parte creativa y musical cuando
pensemos en el audio de un videojuego, los aspectos tcnicos tambin son muy
importantes y harn que una buena pieza musical, unos efectos sonoros realistas y
una buena grabacin de dilogos destaquen adecuadamente en el videojuego.
[990]
30.1.4.
Tcnicas de creacin
Interactividad e inmersin
El sonido y la msica de un videojuego pueden tener diferentes funciones. En
ciertos juegos, el sonido y tiene un papel fundamental, afectando directamente a la
narrativa o al desarrollo del juego. En otros, la msica es un acompaamiento a la
accin y, aunque puede ser ms o menos adaptativa, no constituye un elemento del
juego en s misma.
La msica tambin puede utilizarse para realzar la estructura del videojuego,
utilizando temas principales en momentos diferentes de la accin o ciertos leitmotifs.
Los efectos sonoros pueden tener, adems de su funcin normal de responder
directamente a ciertas acciones del juego, una funcin simblica. En algunos juegos,
los sonidos son utilizados para dar pistas o dirigir al jugador hacia un lugar o para
advertirle de un peligro inminente. Tambin pueden utilizarse para centrar la atencin
del jugador en un elemento concreto de la accin.
Una regla de oro a tener en cuenta a la hora de crear efectos sonoros es que el
sonido ms real no es necesariamente ms verosmil. Intuitivamente nuestro cerebro
puede asociar sonidos a seres fantsticos (de los que no tiene por qu conocer su
sonido) o asociar sonidos irreales a situaciones cotidianas (el golpe de un puetazo en
una pelcula de accin). Lo anterior es aplicable tambin a la msica: qu msica es
ms apropiada para un videojuego ambientado en un mundo virtual?
Nuestro cerebro es capaz de identicar elementos presentes en el videojuego y
asociarlos a pocas histricas, pelculas o experiencias que hacen ms verosmiles
ciertos tipos de msica que otros en cada tipo de juego segn su ambientacin. El
objetivo es, por tanto, crear audio creble por el jugador, ya que dar un valor aadido
a la capacidad de inmersin del videojuego.
Tipos y estilos de msica
En un videojuego, el tipo de msica se diferencia por el momento o parte del
juego en la que suena. Puede ir desde la necesaria para una escena cinemtica, un
tema principal para el ttulo, la msica ambiental que suena durante el juego, escenas
cinemticas, eventos especiales, etc. El estilo de msica depende de las necesidades
del videojuego, siendo muy comn encontrarse en la necesidad de componer muy
diferentes estilos para un mismo proyecto.
Stingers (fanfarrias)
Composicin
Aunque no hay una receta mgica que asegure un buen resultado cuando se trabaja
en la msica de un videojuego, pueden seguirse una serie de buenas prcticas que
harn el trabajo ms fcil.
Crear una paleta de sonidos, escuchar msica de otros autores en el estilo que
pide el videojuego, guardar ideas sueltas que puedan ser tiles en un momento
de poca creatividad, buscar inspiracin en libreras de sonidos, experimentar con
combinaciones de sonidos o conguracin que no hayamos probado antes o, cuando
sea posible, jugar a una versin de prueba del videojuego en el que trabajemos, pueden
ser formas de agilizar el trabajo y estimular la creatividad para encontrar la inspiracin
necesaria.
Son piezas musicales de pocos segundos que dan respuesta a acciones concretas del jugador para llamar su atencin. Para mantener la
ambientacin musical del juego, estas piezas se componen con las mismas caractersticas e instrumentacin que la msica de fondo, pero
son mucho ms llamativas y sonoras.
[991]
Coordinando el trabajo
Una tarea a realizar antes y durante la creacin de efectos sonoros es
mantener un contacto constante con
los desarrolladores y tratar de recopilar la mayor informacin posible que nos pueda ayudar en nuestro
trabajo.
C30
[992]
30.1.5.
Programacin de audio
[993]
Figura 30.5: Algunos motores y APIs de audio vienen acompaadas de herramientas para la creacin y
organizacin del material sonoro de un videojuego. En la imagen, FMOD Designer.
Motores y APIs
En un videojuego, el sonido debe adaptarse a las acciones y decisiones que tome
el jugador. Idealmente, el motor de audio de un videojuego debera ser capaz de
reproducir, pausar y parar un sonido en cualquier instante, cambiar su volumen y
panorama (lugar estereofnico que ocupa), reproducirlo indenidamente, saltar a otro
punto del sonido o msica durante su reproduccin o pasar de una pieza musical a otra
mediante una transicin.
Dado que un videojuego puede desarrollarse para varias plataformas, debe minimizarse la creacin de contenido e implementaciones especcas para cada plataforma,
proporcionando ciertos comportamientos consistentes en todas ellas.
C30
[994]
Los programadores encargados del audio pueden elegir utilizar una de las diferentes herramientas software que existen en el mercado para facilitar su tarea o programar desde cero un motor de sonido. Esta decisin debe tomarse en las primeras fases
del ciclo de creacin del videojuego, para que en el momento en el que el desarrollador o grupo de desarrolladores especializados en audio que llevarn a cabo esta labor
sepan a qu tipo de reto deben enfrentarse.
Desde el punto de vista del creador de contenido, esta informacin tambin
resulta relevante, ya que en multitud de ocasiones sern estas APIs las encargadas
de reproducir este contenido, y porque algunas de ellas vienen acompaadas de
herramientas que debe conocer y ser capaz de manejar.
Existe un amplio nmero de subsistemas de audio en el mercado escritos explcitamente para satisfacer las necesidades de una aplicacin multimedia o un videojuego.
Estos sistemas se incorporan en libreras que pueden asociarse al videojuego tanto esttica como dinmicamente en tiempo de ejecucin, facilitando la incorporacin de un
sistema de audio completo al programador sin conocimientos especializados en este
mbito.
La facilidad de integracin de estas libreras vara segn su desarrollador y la
plataforma a la que van dirigidas, por lo que siempre existir una cierta cantidad de
trabajo a realizar en forma de cdigo que recubra estas libreras para unir el videojuego
con la funcionalidad que ofrecen.
Algunas de las libreras ms utilizadas hoy da son OpenAL, FMOD, Miles
Sound System, ISACT, Wwise, XACT, Beatnik Audio Engine o Unreal 3 Sound
System.
30.2.
30.3.
[995]
Ambas tecnologas tienen mucho en comn. En los grcos 3D los objetos
son matemticamente transformados a coordenadas 2D para su visualizacin en la
pantalla. Las texturas de las supercies de los objetos estn almacenados en imagenes
(mapas de bist) que son adheridas a los objetos.
Cuando se desea mostrar un video por pantalla, un rea determinada de la pantalla
se designa para mostrar el vdeo, ya sea en un rectngulo dentro de una ventana o
a pantalla completa. Este video est formado por fotogramas (gura 30.6), y cada
fotograma es un mapa de bits que se estira para adaptarse al rectngulo de destino.
Como puede verse, la principal diferencia entre grcos 3D y video digital, radica
en que el mapa de bits es actualizado varias veces por segundo en el caso del video,
mientras que la mayora de los modelos 3D usan texturas estticas.
En las prximas secciones se mostrarn algunos conceptos esenciales necesarios
para entender los formatos de video digital para posteriormente pasar a ver cmo
puede realizarse su integracin dentro de escenarios creados mediante Ogre 3D.
30.4.
En este captulo nos centraremos en la integracin del video digital junto a grcos
3D. El video digital es aquel que emplea una representacin digital en lugar de
analgica de la seal de video.
Al hablar de video digital, es conveniente destacar dos factores clave que
intervienen directamente sobre la calidad del vdeo: la captura y el almacenamiento.
La captura de vdeo est fuera del alcance de este captulo. En lo que respecta al
almacenamiento, el video digital puede grabarse directamente sobre soportes digitales
como DVDs, discos duros o memorias ash o bien sobre soportes analgicos como
cintas magnticas y posteriormente distribuirse en soportes digitales.
Para la edicin de video digital, al principio se requera digitalizar una fuente
de vdeo analgica a un formato digital que pudiera manipularse por ordenador.
Posteriormente, el empleo de los formatos digitales para cinta conocidos como DV
y miniDV, posibilit el hecho de grabar directamente en formato digital simplicando
as el proceso de edicin.
C30
[996]
30.5.
[997]
30.5.1.
Instalacin de TheoraVideoPlugin
En primer lugar se debern instalar las libreras Ogg, Vorbis y Theora. Pueden
descargarse de http://xiph.org/downloads/ o podemos emplear los repositorios de paquetes del sistema y usar comandos como:
apt-get install libogg-dev libvorbis-dev libtheora-dev
30.5.2.
C30
[998]
material VideoMaterial {
technique {
pass {
texture_unit {
texture_source ogg_video {
filename clip.ogg
precache 16
play_mode play
}
}
}
}
}
Figura 30.11: Material que incluye textura con vdeo.
30.6.
30.6.1.
para la compilacin y
# pkg-config --libs gstreamer-0.10
para el enlazado.
30.6.2.
[999]
Introduccin a GStreamer
C30
[1000]
gst-launch
gst-inspect
gst-editor
Reproductor
multimedia
Audio/video
conferencia
Servidor de
streamming
Editor de
vdeo
Framework de GStreammer
Clases base
Librera de utilidades
Bus de mensajes
Plugins de sistema
Bindings de lenguajes
...
Pipeline
Gestores de
protocolos:
http, fichero,
rstp, etc.
Fuentes:
Alsa, v4l2,
tcp/udp, etc.
Formatos:
avi, mp4,
ogg, etc.
Filtros:
Codecs:
conversores,
mp3, mpeg4,
mezcladores,
vorbis, etc.
efectos
Sinks:
Alsa, xvideo,
tcp/udp, etc.
Plugins de GStreammer
Plugins de terderos
Para trabajar con la cmara web se emplear v4l2 para capturar frames de la
cmara. Un ejemplo es el siguiente comando donde se captura un frame desde
v4l2src, se procesa por ffmpegcolorspace para que no aparezca con colores extraos,
transforma a formato png con pngenc y se pasa a un chero llamado picture.png con
lesink
# gst-launch-0.10 v4l2src ! ffmpegcolorspace ! pngenc \
! filesink location=picture.png
Si lo que queremos es mostrar todos los frames que recoga la cmara web
directamente en una ventana, podemos hacerlo mediante el siguiente pilepile:
# gst-launch-0.10 v4l2src ! ffmpegcolorspace ! ximagesink
30.6.3.
[1001]
Pads. Son las conexiones de entradas y salidas de los elementos. Cada pad
gestiona datos de tipo especco, es decir, restringen el tipo de datos que
uye a travs de ellos. Son el elemento que permiten crear las pipelines entre
elementos.
Bins y Pipelines. Un bin es un contenedor de elementos. Una pipeline es un tipo
especial de bin que permite la ejecucin de todos los elementos que contiene.
Un bin es un elemento, y como tal puede ser conectado a otros elementos para
construir una pipeline.
Comunicacin. Como mecanismos de comunicacin GStreamer proporciona:
Buffers. Objetos para pasar datos entre elementos de la pipeline. Los
buffers siempre van desde los fuentes hasta los sinks
Eventos. Objetos enviados entre elementos o desde la aplicacin a los
elementos.
Mensajes. Objetos enviados por los elementos al bus de la pipeline.
Las aplicaciones pueden recoger estos mensajes que suelen almacenar
informacin como errores, marcas, cambios de estado, etc.
Consultas. Permiten a las aplicaciones realizar consultas como la duracin o la posicin de la reproduccin actual.
30.6.4.
GStreamer en Ogre
"VideoTexture",
ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
TEX_TYPE_2D,
1, 1,
0, PF_B8G8R8A8,
TU_DYNAMIC_WRITE_ONLY);
mVideoMaterial = MaterialManager::getSingleton().create(
"VideoMaterial",
ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
Ogre::Technique* technique = mVideoMaterial->createTechnique();
technique->createPass();
mVideoMaterial->getTechnique(0)->getPass(0)->
createTextureUnitState(mVideoTexture->getName());
mVideoOverlay = OverlayManager::getSingleton().create("overlay");
OverlayContainer* videoPanel =
static_cast<OverlayContainer*>(OverlayManager::getSingleton().
createOverlayElement("Panel", "videoPanel"));
mVideoOverlay->add2D(videoPanel);
mVideoOverlay->show();
videoPanel->setMaterialName(mVideoMaterial->getName());
C30
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[1002]
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73 }
gst_init(0, 0);
if (!g_thread_supported()){
g_thread_init(0);
}
mPlayer = gst_element_factory_make("playbin2", "play");
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(mPlayer));
gst_bus_add_watch(bus, onBusMessage, getUserData(mPlayer));
gst_object_unref(bus);
mAppSink = gst_element_factory_make("appsink", "app_sink");
g_object_set(G_OBJECT(mAppSink), "emit-signals", true, NULL);
g_object_set(G_OBJECT(mAppSink), "max-buffers", 1, NULL);
g_object_set(G_OBJECT(mAppSink), "drop", true, NULL);
g_signal_connect(G_OBJECT(mAppSink), "new-buffer",
G_CALLBACK(onNewBuffer), this);
GstCaps* caps = gst_caps_new_simple("video/x-raw-rgb", 0);
GstElement* rgbFilter = gst_element_factory_make("capsfilter",
"rgb_filter");
g_object_set(G_OBJECT(rgbFilter), "caps", caps, NULL);
gst_caps_unref(caps);
GstElement* appBin = gst_bin_new("app_bin");
gst_bin_add(GST_BIN(appBin), rgbFilter);
GstPad* rgbSinkPad = gst_element_get_static_pad(rgbFilter,
"sink");
GstPad* ghostPad = gst_ghost_pad_new("app_bin_sink", rgbSinkPad);
gst_object_unref(rgbSinkPad);
gst_element_add_pad(appBin, ghostPad);
gst_bin_add_many(GST_BIN(appBin), mAppSink, NULL);
gst_element_link_many(rgbFilter, mAppSink, NULL);
g_object_set(G_OBJECT(mPlayer), "video-sink", appBin, NULL);
g_object_set(G_OBJECT(mPlayer), "uri", uri, NULL);
gst_element_set_state(GST_ELEMENT(mPlayer), GST_STATE_PLAYING);
4. Captura de mensajes. Para poder recoger los mensajes producidos por playbin2 desde el bus del pipeline, se aadir el mtodo que se invocar cada vez que
se reciba un mensaje mediante la funcin
aplicada sobre el
gst_bus_add_watch
bus, tal y como se muestra en las lneas 38-40 .
[1003]
C30
El otro callback es onNewBuffer en la lnea 49 del mismo listado. Este mtodo
ser invocado por GStreamer cada vez que haya un nuevo frame disponible. Una
posible implementacin puede verse en el listado 30.3 continuacin, donde se pone
un atributo booleano llamado mNewBufferExists a verdadero cada vez que llega un
nuevo frame.
[1004]
gpointer userData){
2
3
4
5
6
7 }
bool GStreamerPlayer::Listener::frameRenderingQueued(
const FrameEvent& evt){
mMediaPlayer->updateVideo();
return ExampleFrameListener::frameRenderingQueued(evt);
}
bool GStreamerPlayer::Listener::frameStarted(
const FrameEvent& evt){
return mMediaPlayer->mRunning &&
ExampleFrameListener::frameStarted(evt);
}
[1005]
TextureManager* mgr=Ogre::TextureManager::getSingletonPtr();
if (!mVideoTexture.isNull()){
mgr->remove(mVideoTexture->getName());
}
mVideoTexture = Ogre::TextureManager::getSingleton().
createManual(
"VideoTexture",
ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
TEX_TYPE_2D,
mVideoWidth, mVideoHeight,
0, PF_B8G8R8A8,
TU_DYNAMIC_WRITE_ONLY);
mVideoMaterial->getTechnique(0)->getPass(0)->
removeAllTextureUnitStates();
mVideoMaterial->getTechnique(0)->getPass(0)->
createTextureUnitState(mVideoTexture->getName());
float widthRatio =
mVideoWidth / static_cast<float>(mWindow->getWidth()) *
pixelRatio;
float heightRatio =
mVideoHeight / static_cast<float>(mWindow->getHeight());
float scale =
widthRatio > heightRatio ? widthRatio : heightRatio;
}
mVideoOverlay->setScale(widthRatio/scale, heightRatio/scale);
HardwarePixelBufferSharedPtr pixelBuffer =
mVideoTexture->getBuffer();
void* textureData = pixelBuffer->lock(
HardwareBuffer::HBL_DISCARD);
memcpy(textureData, GST_BUFFER_DATA(buffer),
GST_BUFFER_SIZE(buffer));
pixelBuffer->unlock();
gst_buffer_unref(buffer);
mNewBufferExists = false;
onNewBuffer.
3-4
.
Seguidamente,
recoger informacin relativa a las dimensiones del vdeo 8-24
y (en nuestro caso) si el vdeo
ha cambiado de tamao crea una nueva texturaborrando
30.7.
C30
En estas secciones del captulo se ha mostrado cmo es posible sacar provecho del
efecto sinrgico entre el mundo de los grcos 3D y el vdeo digital.
[1006]
Captulo
31
Interfaces de Usuario
Avanzadas
Francisco Jurado Monroy
Carlos Gonzlez Morcillo
31.1.
Quiz, lo primero que puede sugerir el trmino Visin por Computador es que
se trata de aquellas tcnicas que permiten ver a travs del ordenador, es decir, ver
como lo hara un ordenador. Lo cierto es que la ciencia ccin se ha encargado de
darnos algunas ideas al respecto, y de algn modo no va desencaminada.
1007
[1008]
31.2.
Introduccin a OpenCV
31.2.1.
Instalacin de OpenCV
[1009]
# apt-get
# apt-get
# apt-get
# apt-get
libcv-dev
install build-essential
install libavformat-dev
install ffmpeg
install libcv2.1 libcvaux2.1 libhighgui2.1 opencv-doc \
libcvaux-dev libhighgui-dev python-opencv
Con esto tendremos los requisitos mnimos para poder desarrollar, compilar y
ejecutar nuestras aplicaciones con OpenCV.
La distribucin viene acompaada de una serie de programitas de ejemplo en
diferentes lenguajes de programacin. Estos pueden resultar de mucha utilidad en
una primera toma de contacto. Si se ha realizado la instalacin mediante el sistema
de paquetes, el paquete opencv-doc es el encargado de aadir dichos ejemplos. Para
poder copiarlos y compilarlos haremos:
#
#
#
#
cp
cd
cd
sh
-r /usr/share/doc/opencv-doc/examples .
examples
c
build_all.sh
31.3.
Conceptos previos
Antes de adentrarnos en los conceptos del procesamiento de imagen que nos ofrece
la Visin por Computador con OpenCV, es preciso introducir algunos conceptos como
aclaraciones sobre las nomenclaturas de las funciones, las estructuras de datos, cmo
construir sencillas interfaces de usuario para testear nuestros desarrollos, etc.
31.3.1.
Nomenclatura
Como nemotcnico general para la interfaz de programacin en C, puede apreciarse cmo las funciones de OpenCV comienzan con el prejo cv seguido de la operacin
y el objeto sobre el que se aplicar. Veamos algunos ejemplos:
cvCreateImage: funcin OpenCV para crear una imagen.
cvCreateMat: funcin OpenCV para crear una matriz.
cvCaptureFromAVI: funcin OpenCV para realizar una captura de un chero
AVI.
cvLoadImage: funcin OpenCV para cargar una imagen.
cvShowImage: funcin OpenCV para mostrar una imagen.
cvConvertImage: funcin OpenCV para convertir una imagen.
cvSetImageROI: funcin OpenCV para establecer una regin de inters (ROI)
sobre una imagen.
C31
...
[1010]
31.3.2.
El mdulo HighGUI, permite la construccin de interfaces de usuario multiplataforma, ligeras y de alto nivel. Estas suelen estr destinadas a probar los procesamientos
que se realicen con OpenCV.
HighGUI tiene funciones para permitir a los desarrolladores interactuar con el
sistema operativo, el sistema de archivos, la WebCam, construir ventanas en las que
mostrar imgenes y vdeos, leer y escribir cheros (imgenes y vdeo) desde y hacia
cheros, gestin sencilla del ratn y teclado, etc.
Ventanas
Las primitivas ms interesantes y comunes en una aplicacin que necesite gestin
de ventanas son:
Crear: cvNamedWindow(window, CV_WINDOW_AUTOSIZE)
Redimensionar: cvResizeWindow(window, width, heigh);
Posicionar: cvMoveWindow(window, offset_x, offset_y)
Cargar imagen: IplImage* img=cvLoadImage(leName)
Visualizar imagen: cvShowImage(window,img);
liberar imagen: cvReleaseImage( &img )
Destruir: cvDestroyWindow(window);
Como puede apreciarse, ninguna de las anteriores primitivas devuelve una referencia a la ventana o recibe por parmetro una referencia. Esto es debido a que HighGUI
accede a las ventanas que gestiona mediante su nombre. De esta manera, cada vez que
se desee acceder a una ventana concreta, habr que indicar su nombre. Esto evita la
necesidad de emplear y gestionar estructuras adicionales por parte de los programadores.
Recibir eventos del teclado
Para el teclado no existe manejador. Tan slo se dispone de una primitiva
cvWaitKey(interval). Si interval vale 0, la llamada es bloqueante, y se quedar
esperando indenidamente hasta que se pulse una tecla. Si interval tiene un valor
positivo, este representar el nmero de milisegundos que esperar a recibir un evento
del teclado antes de continuar con la ejecucin.
Capturando eventos del ratn
Como en la mayora de las libreras para la construccin de interfaces de usuario,
los eventos del ratn son manipulados a travs del correspondiente manejador. Un
ejemplo de manejador del ratn puede verse en el siguiente cdigo:
Listado 31.1: Manejador del ratn
1 void mouseHandler(int event, int x, int y, int flags, void* param){
2
switch(event){
[1011]
3
case CV_EVENT_LBUTTONDOWN:
4
if(flags & CV_EVENT_FLAG_CTRLKEY)
5
printf("Pulsado boton izquierdo con CTRL presionada\n");
6
break;
7
8
case CV_EVENT_LBUTTONUP:
9
printf("Boton izquierdo liberado\n");
10
break;
11
}
12 }
C31
Para registrarlo bastar con invocar a la primitiva cvCreateTrackbar(bar1, window, &trackbarVal ,maxVal , trackbarHandler). Si se desea establecer u obtener la
posicin del trackbar durante un momento dado de la ejecucin, se emplearn las
funciones cvGetTrackbarPos(bar1,window) y cvSetTrackbarPos(bar1, window, value), respectivamente
[1012]
31.3.3.
Estructura de la imagen
/* sizeof(IplImage) */
/* version (=0)*/
/* Most of OpenCV functions support
1,2,3 or 4 channels */
int alphaChannel;
/* Ignored by OpenCV */
int depth;
/* Pixel depth in bits: IPL_DEPTH_8U,
IPL_DEPTH_8S, IPL_DEPTH_16S,
IPL_DEPTH_32S, IPL_DEPTH_32F and
IPL_DEPTH_64F are supported. */
char colorModel[4];
/* Ignored by OpenCV */
char channelSeq[4];
/* ditto */
int dataOrder;
/* 0 - interleaved color channels,
1 - separate color channels.
cvCreateImage can only create
interleaved images */
int origin;
/* 0 - top-left origin,
1 - bottom-left origin (Windows
bitmaps style).*/
int align;
/* Alignment of image rows (4 or 8).
OpenCV ignores it and uses widthStep instead.*/
int width;
/* Image width in pixels.*/
int height;
/* Image height in pixels.*/
struct _IplROI *roi;
/* Image ROI.
If NULL, the whole image is selected. */
struct _IplImage *maskROI;
/* Must be NULL. */
void *imageId;
/* "
" */
struct _IplTileInfo *tileInfo; /* "
" */
int imageSize;
/* Image data size in bytes
(==image->height*image->widthStep
in case of interleaved data)*/
char *imageData;
/* Pointer to aligned image data.*/
int widthStep;
/* Size of aligned image row
in bytes.*/
int BorderMode[4];
/* Ignored by OpenCV.*/
int BorderConst[4];
/* Ditto.*/
char *imageDataOrigin; /* Pointer to very origin of image data
(not necessarily aligned) needed for correct deallocation */
}
IplImage;
As, para acceder a un pixel concreto para una imagen multicanal puede emplearse
el siguiente cdigo:
Listado 31.4: Acceso a los pixels de una imagen
1
2
3
4
5
6
7
8
9
IplImage* img
= cvCreateImage(cvSize(640,480),IPL_DEPTH_32F,3);
int height
int width
int step
int channels
float * data
=
=
=
=
=
img->height;
img->width;
img->widthStep/sizeof(float);
img->nChannels;
(float *)img->imageData;
data[i*step+j*channels+k] = 111;
[1013]
Del mismo modo, para acceder a todos los pixels de una imagen, bastar con
implementar un bucle anidado, tal y como se muestra en el siguiente cdigo de
ejemplo encargado de convertir una imagen a escala de grises:
Listado 31.5: Operacin directa sobre pixels. Convirtiendo a escala de grises.
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
#include "highgui.h"
#include "cv.h"
#include "stdio.h"
int main(int argc, char **argv) {
IplImage *img = cvLoadImage( argv[1], CV_LOAD_IMAGE_COLOR );
if (!img)
printf("Error while loading image file\n");
int width
int height
int nchannels
int step
uchar *data
=
=
=
=
=
img->width;
img->height;
img->nChannels;
img->widthStep;
( uchar* )img->imageData;
int i, j, r, g, b, byte;
for( i = 0 ; i < height ; i++ ) {
for( j = 0 ; j < width ; j++ ) {
r = data[i*step + j*nchannels + 0];
g = data[i*step + j*nchannels + 1];
b = data[i*step + j*nchannels + 2];
byte = ( r + g + b ) / 3;
31.3.4.
return 0;
C31
[1014]
Con esto se dispone de todo lo necesario para poder trabajar con memoria
dinmica en OpenCV.
Secuencias: CVSeq
Un tipo de objeto que puede ser guardado en los almacenes de memoria son las
secuencias. Estas son listas enlazadas de otras estructuras, de manera que se pueden
crear secuencias de cualquier tipo de objeto. Por su implementacin, las secuencias
permiten incluso el acceso directo a cada uno de los elementos.
Algunas de las principales primitivas para su manipulacin son:
Crear: CvSeq* cvCreateSeq(int seq_ags,
int header_size, int elem_size, CvMemStorage* storage)
Eliminar: void cvClearSeq(CvSeq* seq)
Acceder: char* cvGetSeqElem(seq,index)
Comprobar: int cvSeqElemIdx(const CvSeq* seq,
const void* element, CvSeqBlock** block = NULL)
Clonar: CvSeq* cvCloneSeq(const CvSeq* seq,
CvMemStorage* storage)
Ordenar: void cvSeqSort(CvSeq* seq, CvCmpFunc func,
void* userdata = NULL)
Buscar: char* cvSeqSearch(CvSeq* seq,
const void* elem, CvCmpFunc func, int is_sorted,
int* elem_idx, void* userdata = NULL)
31.4.
[1015]
Listado 31.6: Mostrando imgenes de una captura
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 "highgui.h"
#include "cv.h"
int main( int argc, char** argv ) {
CvCapture* capture;
if( argc != 2 || !(capture = cvCreateFileCapture(argv[1])) )
capture = cvCreateCameraCapture(0);
cvNamedWindow( "Window", CV_WINDOW_AUTOSIZE );
IplImage* frame;
while(1) {
frame = cvQueryFrame(capture);
if(!frame) break;
cvShowImage("Window", frame);
char c = cvWaitKey(33);
if(c==27) break;
cvReleaseCapture(&capture);
cvDestroyWindow("Window");
}
return 0;
esto, puede que el cdigo resulte bastante autodescriptivo. As, las lneas
Aclarado
1-2
muestran
la inclusin de los cheros con las deniciones de las funciones tanto
para OpenCV como para Highgui.
Lo primero que hace la aplicacin es una mnima
comprobacin de parmetros 6-7 . Si se ha introducido un parmetro, se entender
que es la ruta al chero a visualizar y se crear una captura para extraer la informacin
del mismo mediante la llamada a cvCreateFileCapture. En caso de que no se hayan
introducido parmetros o el parmetro no sea un chero de video, se realizar la
captura desde la WebCam empleando la llamada a cvCreateCameraCapture.
A continuacin, en la lnea 9 se crear una ventana llamada Window mediante
la llamada a cvNamedWindow. Como puede verse, esta funcin no devuelve ningn
puntero a la estructura que permita acceder a la ventana, sino que construye una
ventana con nombre. As, cada vez que deseemos acceder a una ventana concreta,
bastar con indicar su nombre, evitando emplear estructuras adicionales.
31.5.
C31
[1016]
#include "highgui.h"
#include "cv.h"
int main( int argc, char** argv ) {
CvCapture* capture;
if( argc != 2 || !(capture = cvCreateFileCapture(argv[1])) )
capture = cvCreateCameraCapture(0);
cvNamedWindow( "Window_1", CV_WINDOW_AUTOSIZE );
cvNamedWindow( "Window_2", CV_WINDOW_AUTOSIZE );
IplImage* frame;
while(1) {
frame = cvQueryFrame( capture );
if( !frame ) break;
IplImage
smooth =
edge
=
out
=
char c = cvWaitKey(33);
if( c == 27 ) break;
cvReleaseImage(&frame);
cvReleaseCapture( &capture );
cvDestroyWindow( "Window_1" );
cvDestroyWindow( "Window_2" );
}
return 0;
En las lneas 9-10 se han creado dos ventanas, de manera que se puedan apreciar
los frames de entrada y de salida del ejemplo. Para almacenar las transformaciones
intermedias,
se crearn una serie de imgenes mediante la primitiva cvCreateImage
El proceso de ltrado est codicado en las lneas 22-24 . Como puede apreciarse
son tres los ltros que se le aplican:
[1017]
1. Smooth o suavizado. El primer paso es el de aplicar a cada frame capturado una
operacin de suavizado Gaussiano 11x11. Este ltro producir una imagen ms
suave a la que se le ha eliminado el posible ruido y cambios bruscos en el
color.
2. Escala de grises. Esta transformacin permite pasar la imagen de RGB a escala
de grises. De ah que la imagen creada para almacenar
esta transformacin
intermedia tenga una profundidad de 1 en la lnea 19 . Este es en realidad un
paso intermedio requerido para generar la entrada al algoritmo de deteccin de
contornos.
3. Canny o deteccin de contornos. El cul permite aplicar el algoritmo Canny
para la identicacin de contornos en una imagen. Este algoritmo toma como
entrada una imagen de un solo canal (la imagen en escala de grises del paso
anterior), y genera otra imagen de un solo canal en la que se almacenan las
siluetas o contornos identicados.
En la gura 31.4 puede verse cul ser la salida nal de la ejecucin de nuestra
aplicacin.
Figura 31.4: Salida del ejemplo. A la izquierda la imagen antes del proceso de ltrado. A la derecha los
bordes detectados tras el proceso de ltrado.
C31
1
2
3
4
5
6
7
8
9
10
[1018]
11
CV_RETR_TREE,
12
CV_CHAIN_APPROX_SIMPLE, cvPoint(0,0) );
13 cvZero(gray);
14 if( contours )
15
cvDrawContours(gray, contours, cvScalarAll(255),
16
cvScalarAll(255), 100, 3, CV_AA, cvPoint(0,0) );
17 cvShowImage( "Contours", gray );
31.6.
31.6.1.
[1019]
40, 50 ));
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 }
CvRect* r; int i;
for( i = 0; i < (faces? faces->total : 0); i++ ){
r = (CvRect*)cvGetSeqElem(faces, i);
cvRectangle(img, cvPoint(r->x, r->y),
cvPoint(r->x + r->width, r->y + r->height),
colors[i %8], 3, 10, 0);
}
cvNamedWindow("Window", CV_WINDOW_AUTOSIZE );
cvShowImage("Window", img );
cvWaitKey(0);
cvReleaseImage(&img);
cvDestroyWindow("Window");
return 0;
En la lnea 5 se denen una serie de colores que sern
empleados
para marcar
con un rectngulo los objetos detectados. En las lneas 10-12 , cargamos la imagen
desde un chero, reservamos memoria para llevar a cabo el proceso de clasicacin y
cargamos la especicacin del clasicador.
C31
[1020]
31.6.2.
Una vez visto cmo realizar deteccin de objetos con OpenCV, a nadie se le
escapa que el hecho de identicar un cuerpo humano, un rostro (o cualquier otra parte
del cuerpo humano), etc., puede permitir a nuestras aplicaciones realizar acciones en
funcin de dnde se encuentren dichos objetos.
As, imagne que pudieramos ejecutar unas acciones u otras segn la posicin
del usuario ante la cmara, o que atendiendo al parpadeo de ste fuera posible
desencadenar la realizacin de unas acciones u otras en el computador. Introducir
los conceptos asociados a estas cuestiones es el objeto de esta subseccin.
[1021]
Llegados a este punto, el lector entender gran parte del cdigo, de modo que
nos centraremos en aquellas partes que resultan nuevas. As, en el bucle que
recoge
los frames de la captura, el clasicador es cargado slo la primera vez 32-34 . A
continuacin,
a escala
para
cada frame capturado, se realizar una transformacin
la
imagen
a
otra
ms
pequea
39-42
y
nalmente
de grises 36-37 , se reducir
se ecualizar el histograma 43 . Este ltimo paso de ecualizar el histograma, sirve
para tratar de resaltarn detalles que pudieran haberse perdido en los procesos de
transformacin a escala de grises y reduccin de tamao. Tras nalizar el proceso
59-64
,
adems
de
identicarlo
mediante
un
recuadro
66
.
Listado 31.10: Interaccin mediante reconocimiento
#include "highgui.h"
#include "cv.h"
#include "stdio.h"
int main( int argc, char** argv ) {
CvCapture* capture;
if( argc != 2 || !(capture = cvCreateFileCapture(argv[1])) )
capture = cvCreateCameraCapture(0);
static CvScalar colors[] = { {{0,0,255}},
{{0,128,255}},
{{0,255,255}}, {{0,255,0}},
{{255,128,0}}, {{255,255,0}},
{{255,0,0}},
{{255,0,255}} };
cvNamedWindow( "Window", CV_WINDOW_AUTOSIZE );
IplImage *frame, *gray, *small_img;
double scale = 2;
CvMemStorage* storage = cvCreateMemStorage(0);
cvClearMemStorage( storage );
CvHaarClassifierCascade* classifier = NULL;
CvFont font;
cvInitFont(&font, CV_FONT_HERSHEY_SIMPLEX,
0.75, 0.75, 0, 2, CV_AA);
while(1) {
frame = cvQueryFrame(capture);
if( !frame ) break;
if (classifier == NULL)
classifier = cvLoadHaarClassifierCascade
("./media/haarcascade_frontalface_alt.xml", cvGetSize(frame));
gray = cvCreateImage( cvGetSize(frame), 8, 1 );
cvCvtColor( frame, gray, CV_BGR2GRAY );
small_img = cvCreateImage(
cvRound
8, 1 );
cvResize( gray, small_img,
cvEqualizeHist( small_img,
C31
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
[1022]
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 }
char c = cvWaitKey(5);
if(c == 27) break;
cvReleaseCapture(&capture);
cvDestroyWindow("Window");
return 0;
Figura 31.6: La aplicacin muestra cundo ha identicado un rostro y si este se encuentra a la izquierda o
a la derecha.
31.7.
[1023]
C31
1 #include <cv.h>
2 #include <highgui.h>
3
4 int main(){
5
CvCapture *capture = cvCaptureFromCAM(0);
6
7
if(!capture) return -1;
8
9
cvNamedWindow("Window", CV_WINDOW_FULLSCREEN);
10
11
IplImage* frame = NULL;
12
IplImage* imgScribble = NULL;
13
while(1){
14
frame = cvQueryFrame(capture);
15
if(!frame) break;
16
17
if(imgScribble == NULL)
18
imgScribble = cvCreateImage(cvGetSize(frame), 8, 3);
19
20
cvSmooth(frame, frame, CV_GAUSSIAN, 11, 11, 0, 0);
21
22
IplImage* imgHSV = cvCreateImage(cvGetSize(frame), 8, 3);
23
cvCvtColor(frame, imgHSV, CV_BGR2HSV);
24
25
IplImage* imgThreshed = cvCreateImage(cvGetSize(frame), 8, 1);
26
cvInRangeS(imgHSV, cvScalar(25, 100, 100, 0),
27
cvScalar(40, 255, 255, 0), imgThreshed);
28
29
CvMoments *moments = (CvMoments*)malloc(sizeof(CvMoments));
30
cvMoments(imgThreshed, moments, 0);
31
32
double moment10 = cvGetSpatialMoment(moments, 1, 0);
33
double moment01 = cvGetSpatialMoment(moments, 0, 1);
34
double area
= cvGetCentralMoment(moments, 0, 0);
35
36
static int posX = 0; static int posY = 0;
37
int lastX = posX; int lastY = posY;
38
posX = moment10/area; posY = moment01/area;
39
40
if(lastX>0 && lastY>0 && posX>0 && posY>0)
41
cvLine(imgScribble,
42
cvPoint(posX, posY), cvPoint(lastX, lastY),
43
cvScalar(255, 255, 0, 0), 5, 1, 0);
44
45
cvAdd(frame, imgScribble, frame, 0);
46
cvShowImage("Window", frame);
47
[1024]
48
49
50
51
52
53
54
55
56
57
58
59
60 }
cvReleaseImage(&imgHSV);
cvReleaseImage(&imgThreshed);
free(moments);
cvReleaseCapture(&capture);
cvDestroyWindow("Window");
return 0;
En l puede verse cmo, una vez seleccionado el color del objeto a detectar, el
procedimiento a llevar a cabo es el siguiente (la salida de su ejecucin aparece en la
gura 31.9):
1. Realizar
un proceso de suavizado para eliminar el posible ruido en el color
21 .
2. Convertir la imagen RGB del frame capturado a una imagen HSV (Hue,
Saturation, Value) (Matiz, Saturacin,
Valor) de modo que sea ms fcil
3. Eliminar todos aquellos pixels cuyo matiz no corresponda con el del objeto a
detectar. Esto devolver una imagen en la que sern puestos a 0 todos los pixels
que no estn dentro de nuestro criterio. En denitiva,
4. Identicar el momento
de los objetos en movimiento, es decir, su posicin en
coordenadas 29-34 .
5. Pintar una linea desde el anterior momento hasta el momento actual 29-43
en una capa (en el cdigo es una imagen denominada imgScribble).
Figura 31.8: A la izquierda, se muestra la imagen HSV del Tux capturado con la cmara. A la derecha slo
aquello que coincida con el valor del color de las patas y el pico (amarillo en nuestro caso)
[1025]
Figura 31.9: Salida de la ejecucin del ejemplo de deteccin de objeto con determinado color. Puede verse
cmo se superpone a la imagen el trazo realizado por el rastreo de un pequeo objeto amarillo uorescente.
31.8.
3 );
cvZero( motion );
motion->origin = image->origin;
}
C31
10
11
12
13
14
15
16
17
[1026]
18
19
break;
Para realizar el anlisis de dicho cdigo debemos comenzar por el bucle principal.
En l puede verse cmo se captura la imagen, y si an no existe movimiento
detectado, se crea una nueva
imagen cuyo origen se har coincidir con el del primer
grayscale
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 }
sta proporciona una imagen donde la salida es la diferencia de los pixels entre una
imagen y la siguiente. Aquellos pixels con diferencia distinta de cero, son los que han
cambiado
y por tanto los que se han movido. Bastar con aplicar un ltrado threshold
binario 14 al resultado de la operacin anterior, de manera que todos aquellos pixels
cuya diferencia sea mayor que un determinado umbral tomarn el valor 1 y en caso
contrario el valor 0. En este punto se dispone de la silueta de aquella parte de la
imagen que se ha movido (aqu la imagen llamada silh).
El siguiente paso es el de actualizar la imagen del movimiento con la silueta
obtenida. Para ello se emplea la primitiva cvUpdateMotionHistory que recibe como
parmetros:
[1027]
Mscara de la silueta que contiene los pixels que no estn puestos a 0 donde
ocurri el movimiento.
Historia de imgenes del movimiento, que se actualiza por la funcin y debe ser
de un nico canal y 32 bits.
Hora actual.
Duracin mxima para el rastreo del movimiento.
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
C31
[1028]
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
cvResetImageROI(
cvResetImageROI(
cvResetImageROI(
cvResetImageROI(
56
57
58
mhi );
orient );
mask );
silh );
Figura 31.10: Deteccin del movimiento capturado. En azul aquellos pixels donde se ha localizado
movimiento. En negro el fondo esttico.
[1029]
su interior para indicar el ngulo del movimiento 51-57 . En la gura 31.10 puede
encontrarse una salida con la imagen en azul del MHI y el resto de elementos estticos
en negro.
31.9.
31.10.
31.11.
C31
Tres acelermetros para medir la fuerza ejercida en cada uno de los ejes.
[1030]
2. Elementos actuadores
Cuatro luces LED (Light Emitter Diode) localizados en la zona inferior.
Un pequeo altavoz para emitir sonidos al usuario en base a respuestas de
la aplicacin, y que se encuentra en la zona central del mando.
Un motor de vibracion para enviar zumbidos al usuario en funcin de la
interaccin de este.
3. Interfaces de comunicacin
Un puerto de expansion para permitir conectar otros dispositivos (como el
Nunchuk) en el frontal inferior.
Una interfaz de conectividad Bluetooth, gracias a la cul es posible
emparejarlo con cualquier computador e implementar aplicaciones que
interaccionen con l.
[1031]
A travs del puerto de expansin situado en la parte inferior del mando, pueden
conectarse una serie de dispositivos adicionales para permitir incrementar las posibilidades de interaccin que proporciona. Entre estos dispositivos caben destacar el
Nunchunk, el cul consiste en un joystick analgico con dos botones ms (comnmente utilizado para controlar el movimiento de los personajes), la Wii Wheel, que
transforma el mando en un volante para aquellos juegos de simulacin de vehculos,
o el Wii Zapper, que convierte al Wiimote+Nunchuk en una especie de pistola.
El objetivo de todos estos elementos es siempre el mismo: proporcionar una interaccin lo ms cmoda y natural posible al usuario a partir de sensores y actuadores
que obtengan informacin acerca del movimiento que ste est realizando.
Interpretar esta informacin de forma correcta y crear los manejadores de eventos
apropiados para programar la aplicacin en funcin de las necesidades de interaccin,
corre a cargo de los programadores de dichas apliciaciones. Para podeer capturar
desde nuestras aplicaciones la informacin procedente de un dispositivo Wiimote, este
dispone del puerto Bluetooth. As, podrn programarse sobre la pila del protocolo
Bluetooth las correspondientes libreras para comunicarse con el mando. En el
siguiente apartado se abordar cmo trabajar con ste tipo de libreras.
31.12.
C31
1 typedef struct {
2
wiimote_mode_t mode; /* Current report mode of wiimote. */
3
wiimote_keys_t keys;
/* Current key state. */
4
wiimote_point3_t axis; /* Current accelerometer data. */
[1032]
5
wiimote_ir_t ir1;
/* First detected IR source. */
6
wiimote_ir_t ir2;
/* Second detected IR source. */
7
wiimote_ir_t ir3;
/* Third detected IR source. */
8
wiimote_ir_t ir4;
/* Fourth detected IR source. */
9
10
wiimote_cal_t cal;
/* Wiimote calibration data */
11
wiimote_ext_port_t ext; /* Current extension port state. */
12
13
wiimote_link_t link;
/* Current link state. */
14
wiimote_led_t led;
/* Current state of the leds. */
15
uint8_t rumble;
/* Current state of rumble. */
16
uint8_t speaker;
/* ... */
17
uint8_t battery;
/* Current battery status. */
18
19
wiimote_float3_t tilt; /* The tilt of the wiimote in degrees*/
20
wiimote_float3_t force; /* The force in g on each axis. */
21
22
struct {
23
wiimote_mode_t mode;
24
wiimote_keys_t keys;
25
wiimote_led_t led;
26
uint8_t rumble;
27
} old;
28 } __attribute__((packed)) wiimote_t;
Puede verse cmo quedan representados los botones, los acelermetros, ngulos
de inclinacin, fuerzas de gravedad, el altavoz, la batera, etc.
Bucle principal de captura de datos y comunicacin con el mando
Toda aplicacin que manipule el Wiimote deber contar con un bucle de captura/envo de datos desde y hacia el mando. Este tiene el aspecto que se muestra en el
listado analizado a continuacin.
wiimote_connect 6 , a la que se le pasa una referencia a la anterior estructura y la
direccin MAC del mando. Si desconocemos la direccin del mando, en GNU/Linux
podemos emplear el comando hcitool mientras pulsamos simultaneamente los botones
1 y 2 del mando.
# hcitool scan
#include "wiimote_api.h"
...
wiimote_t wiimote = WIIMOTE_INIT;
if (wiimote_connect(&wiimote, "XX:XX:XX:XX:XX:XX")<0){
fprintf(stderr, "Error al conectar con el mando: %s\n",
wiimote_get_error());
[1033]
exit(1);
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 }
return 0;
C31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[1034]
+Z
+Y
Pitch (cabeceo)
M
IIW
e
ot
-Y
-X
-Z
Roll (balanceo)
[1035]
#include <stdio.h>
#include <stdlib.h>
#include "wiimote_api.h"
int main(int argc, char **argv) {
if (argc!=2){
printf("Introduzca la mac del mando");
exit(0);
}else{
printf("Pulse 1+2 para sincronizar");
}
wiimote_t wiimote = WIIMOTE_INIT;
wiimote_connect(&wiimote, argv[1]);
wiimote.mode.acc = 1; // habilitar acelerometro
while (wiimote_is_open(&wiimote)) {
wiimote_update(&wiimote);
if (wiimote.keys.home) {
wiimote_disconnect(&wiimote);
}
}
}
return 0;
C31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[1036]
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 }
if (wiimote.keys.up)wiimote.led.one = 1;
else wiimote.led.one = 0;
if (wiimote.keys.down) wiimote.led.two = 1;
else wiimote.led.two = 0;
if (wiimote.keys.left) wiimote.led.three = 1;
else wiimote.led.three = 0;
if (wiimote.keys.right) wiimote.led.four = 1;
else wiimote.led.four = 0;
if (wiimote.keys.a) wiimote.rumble = 1;
else wiimote.rumble = 0;
return 0;
while (wiimote_is_open(&wiimote)) {
wiimote_update(&wiimote);
if (wiimote.keys.home) {
wiimote_disconnect(&wiimote);
}
if (wiimote.force.y>0)
printf ("< %.3f grados ", wiimote.force.y);
else if (wiimote.force.y<0)
printf ("> %.3f grados ", wiimote.force.y);
else
printf ("= %.3f grados ", wiimote.force.y);
if (wiimote.keys.one){
printf ("frenando");
wiimote.rumble = 1;
}else{
wiimote.rumble = 0;
}
if (wiimote.keys.two) printf ("acelerando");
if (wiimote.keys.a)
printf ("nitro");
[1037]
printf ("\n");
Suponga que desea implementar un juego donde se deba lanzar una caa de
pescar o simular el golpe de un palo de golf. Deber tomar e interpretar del
modo adecuado el viraje de la mano. As, para el caso de la caa de pescar se
monitorizar la fuerza de la gravedad ejercida en el eje z para saber si est hacia
adelante y hacia atrs, siempre y cuando la inclinacin en el eje y sea negativo
(la caa debe estar arriba). Adems, deber calcularse la fuerza a partir de las
componentes del vector gravedad.
Listado 31.21: Usando el mando como caa de pescar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (wiimote_is_open(&wiimote)) {
wiimote_update(&wiimote);
if (wiimote.keys.home) {
wiimote_disconnect(&wiimote);
}
C31
[1038]
Como puede verse las posibilidades son mltiples, solo es cuestin de identicar
cules son los datos procedentes del mando que son imprescindibles para nuestra aplicacin, posibilitndo de este modo la construccin de interfaces de usuario naturales,
donde un elemento del mundo real externo a la aplicacin, puede interaccionar con el
mundo virtual dentro del computador.
Si en ocasiones no resulta evidente el conocer qu elementos deben procesarse
y con qu valores, existen aplicaciones que pueden ayudar a testear cules son las
variables que intervienen en un movimiento. En la gura 31.13 puede verse el aspecto
de una de ellas: WmGui. Se trata de una aplicacin para GNU/Linux donde podemos
monitorizar parmetros como los botones pulsados, la variacin sobre los ejes x, y, z,
la acceleracin como composicin del vector gravedad, la rotacin, el balanceo, etc.
31.13.
[1039]
El sensor de profuncidad consiste en una convinacin de un laser infrarrojo
y un sensor monocromo CMOS (Complementary Metal-Oxide-Semiconductor) de
infrarrojos. Esto le permite capturar video 3D bajo culquier tipo de luz ambiental. El
laser infrarrojo se proyecta sobre cada uno de los objetos que se encuentre a su alcance.
El sensor recibe el infrarrojo reejado por los objetos. Analizando las longitudes de
onda de la seal reejada, el dispositivo calcula la distancia a la que se encuentran los
objetos.
Kinect cuenta adems con un motor en su base, el cual le permite modicar su
inclinacin. Adems dispone de una conexin USB (Universal Serial Bus), gracias a
la cul es posible comunicarnos con l e implementar aplicaciones que manipulen los
datos que proporciona.
Hasta ahora Kinect no haba tenido muchos rivales, sin embargo, cabe mencionar
un par de competidores interesantes: ASUS Xtion Motion y PrimeSense Carmine. El
primero ha sido desarrollado por ASUS y PrimeSense; el segundo, ntegramente por
PrimeSense, la empresa que dise el sensor de profundidad de Kinect.
Desde el punto de vista de la programacin, Kinect cuenta con un API desarrollada
por Microsoft capaz de realizar deteccin y seguimiento de objetos como cuerpo,
cabeza, manos, identicacin de gestos, etc. Adems, desde su aparicin, tanto la
liberacin por parte de PrimeSense del cdigo de los drivers del sensor de profuncidad,
como la labor de ingeniera inversa desarrollada por algunas comunidades, han
permitido que los desarrolladores puedan implementar APIs con las que poder
manipular el dispositivo Kinect empleando alternativas libres.
En las siguientes secciones se abordarn algunas de las principales APIs y
Middlewares de libre disposicin ms empleadas y robustas.
31.14.
Comunidad OpenKinect
OpenKinect 1 es una comunidad interesada en manipular Kinect en sus aplicaciones. Desarrollan libreras de cdigo abierto bajo licencia Apache2.0 y GPL2, para
poder implementar aplicaciones que hagan uso de Kinect desde sistemas Windows,
Linux y Mac. Su principal desarrollo es la librera libfreenect.
El cdigo de libfreenect se encuentra disponible en la direccin https://
github.com/OpenKinect/libfreenect, aunque se pueden encontrar los
paquete para sistemas Ubuntu/Debian a travs del correspondiente sistema de gestin
de paquetes. As, para instalar libfreenect, el paquete de desarrollo con sus cabeceras
y las demos, bastar escribir desde consola:
# sudo add-apt-repository ppa:floe/libtisch
# sudo apt-get update
# sudo apt-get install freenect libfreenect libfreenect-dev
libfreenect-demos
libfreenect requiere que el usuario que accede a Kinect pertenezca a los grupos
plugdev y video. Deber garantizarse que el usuario pertenece a estos grupos mediante
los comandos:
# sudo adduser <USER> plugdev
# sudo adduser <USER> video
C31
1 http://openkinect.org/wiki/Main_Page
[1040]
Tras esto, tendr que cerrarse la sesin para volver a acceder de nuevo y hacer as
efectivos los grupos a los que se ha aadido el usuario. Con ello, podrn ejecutarse
las aplicaciones que manipulen Kinect, como las que se incluyen en el paquete de
demostracin para probar la captura realizada por las cmaras de Kinect:
# freenect-glview
Con todo, lo que OpenKinect proporciona con libfreenect es un API para el acceso
a los datos procedentes de Kinect y para el control del posicionamiento del motor.
Sin embargo, procesar los datos ledos es responsabilidad de la aplicacin, por lo
que es comn su empleo junto a libreras como OpenCV para la identicacin del
movimiento, reconocimiento de personas, etc.
Si lo que se desea es poder disponer de un API de ms alto nivel, deberemos hacer
uso de algn framework, como el de OpenNI que pasaremos a describir en la siguiente
seccin.
31.15.
OpenNI
31.15. OpenNI
[1041]
Capa de aplicacin
Juegos
Navegadores
etc.
Middleware OpenNI
Capa de Hardware
Dispositivos y sensores
Micrfonos
Videocmaras
Kinect
C31
[1042]
31.15.1.
Instalacin
Para poder emplear OpenNI, deber descargarse el software OpenNI, el middleware NiTE de PrimeSense y el driver del Sensor de la direccin http://www.
openni.org/openni-sdk/, donde pueden encontrarse los paquetes preparados
para Windows, Linux y Mac, as como el acceso a los repositorios github de los fuentes para ser compilados e instalados.
Sin embargo, si se desea desarrollar aplicaciones que empleen OpenNI en otras
plataformas, una opcin interesante y sencilla puede pasar por seguir el proyecto
Simple-OpenNI2 , donde se pone a nuestra disposicin una versin algo reducida del
cdigo de OpenNI con un wrapper para Processing, pero con funcionalidad suente
para realizar una primera inmersin en el potencial de OpenNI.
As, desde la pgina de Simple-OpenNI pueden descargarse cheros zip con el
cdigo preparado para compilar e instalar en sistemas Linux de 64 bits, MacOSX
y Windows de 32 y 64 bits. Una vez descargado el chero zip para la plataforma
correspondiente, bastar descomprimirlo y se dispondr de directorios llamados
OpenNI-Bin-Dev-Linux-x64-v1.5.2.23, NITE-Bin-Dev-Linux-x64-v1.5.2.21 y SensorBin-Linux-x64-v5.1.0.41 si nos hemos descargado el cdigo para una arquitectura de
64 bits.
En cualquiera de los casos, si se ha descargado el zip completo desde SimpleOpenNI o si se ha descargado desde los correspondientes repositorios github, para la
instalacin debern compilarse e instalarse por separado cada uno de los anteriores
paquetes, invocando al ./install que se encuentra en cada uno de ellos:
1. OpenNI
# cd OpenNI-Bin-Dev-Linux-x64-v1.5.2.23
# sudo ./install
2. Nite
# cd NITE-Bin-Dev-Linux-x64-v1.5.2.21
# sudo ./install
3. Driver Kinect
# cd Sensor-Bin-Linux-x64-v5.1.0.41
# sudo ./install
31.15.2.
Conceptos iniciales
Una vez instalado el software, antes de comenzar adentrndonos en las particularidades del framework OpenNI, deben denirse una serie de conceptos iniciales necesarios para comprender cmo programar aplicaciones que lo utilicen.
Mdulos
Como se ha mencionado, el framework OpenNI proporciona una capa de abstraccin tanto para dispositivos fsicos como para otros componentes del middleware.
Estos son conocidos como mdulos. OpenNI distingue dos grupos de mdulos:
2 http://code.google.com/p/simple-openni/
31.15. OpenNI
[1043]
Mdulos sensores: encargados del sensor 3D, de la cmara RGB, de la cmara
IR y de los micrfonos.
Componentes del middleware: destinados al anlisis y deteccin del cuerpo,
puntos de la mano, deteccin de gestos, anlisis de escena, etc.
Nodos de produccin y generadores
Los nodos de produccin son componentes que encapsulan una funcionalidad muy
concreta. Estos toman unos datos de un tipo concreto en su entrada, para producir unos
datos especcos de salida como resultado de un procesamiento concreto. Esta salida
puede ser aprovechada por otros nodos de produccin o por la aplicacin nal.
Existen nodos de produccin tando de los sensores como del middleware.
Aquellos nodos de produccin que proporcionan datos que pueden ser manipulados
directamente por nuestra aplicacin son los conocidos como generadores. Ejemplos
de generadores son: el generador de imagen, que proporciona la imagen capturada
por la cmara; el generador de profundidad, que proporciona el mapa de profundidad
capturado por el sensor 3D; el generador de usuario, que proporciona datos sobre
los usuarios identicados; el generador de puntos de la mano, que proporciona
informacin acerca del muestreo de las manos; el generador de alertas de gestos, que
proporciona datos a cerca de los gestos identicados en los usuarios, etc.
Cadenas de produccin
Es la encadenacin de varios nodos de produccin de manera que la salida del
procesamiento de uno es empleada como entrada para el procesamiento de otro, a n
de obtener el resultado nal deseado.
31.15.3.
Para conocer cul es el modo habitual de proceder cuando se codiquen aplicaciones que manipulen Kinect empleando OpenNI, veremos paso a paso un sencillo
ejemplo donde se leen algunos datos del esqueleto de una persona. Comenzaremos
analizando el cdigo que se muestra en el listado.
Listado 31.22: Ej. sencillo para reconocimiento de la mano izquierda
C31
1 int main(){
2
XnStatus nRetVal = XN_STATUS_OK;
3
4
//Inicializacin del contexto
5
xn::Context context;
6
nRetVal = context.Init();
7
8
// Creacin de los generadores necesarios
9
nRetVal = g_UserGenerator.Create(context);
10
11
// Establecer las opciones de los generadores
12
XnCallbackHandle h1, h2, h3;
13
g_UserGenerator.RegisterUserCallbacks(User_NewUser,
14
User_LostUser, NULL, h1);
15
g_UserGenerator.GetPoseDetectionCap().RegisterToPoseCallbacks(
16
Pose_Detected, NULL, NULL, h2);
17
g_UserGenerator.GetSkeletonCap().RegisterCalibrationCallbacks(
18
Calibration_Start,
19
Calibration_End, NULL, h3);
20
[1044]
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
52
53
54
55
56
57 }
printf("\n");
}
}
context.Shutdown();
return 0;
31.15. OpenNI
[1045]
producidos por los generadores. En dicho bucle, lo primero que se har es actualizar
la informacin procedente de los generadores
mediante alguna de las funciones de la
familia WaitAndUpdateAll() del Contexto 29 . A continuacin, se tendr disponible la
informacin proporcionada por los generadores, y que podr recogerse con mtodos
Get, los cuales dependern del generador o generadores que se estn manipulando.
En el ejemplo, lo que se hace es invocar al mtodo GetUsers 34 que devuelve el Id
para cada uno de los usuarios que ha reconocido el generador, as como el nmero de
usuarios detectados en la escena. Con esta informacin, para cada uno de los usuarios
identicados, se va recogiendo su esqueleto mediante el mtodo GetSkeletonCap(),
y a partir de este, la posicin en la que se encuentra la mano izquierda con el
mtodo GetSkeletonJointPosition() 38 al que se le ha indicado qu parte del esqueleto
se desea extraer (mediante el parmetro XN_SKEL_LEFT_HAND) y dnde se
almacenar la informacin (en la estructura LeftHand).
A
partir de aqu, bastar con
de abordar de nuevo este fragmento del cdigo. As, en el ejemplo, en las lneas 11-22
puede verse cmo se han registrado determinados manejadores XnCallbackHandle
que indican al generador de usuario qu accin debe realizar cuando se ha detectado
un nuevo usuario o cuando se ha perdido uno (empleando para ello el mtodo
RegisterUserCallbacks), qu hacer cuando se ha registrado una determinada pose del
usuario (mediante RegisterToPoseCallbacks), y las acciones a realizar al comenzar y
terminar el proceso de calibracin (usando RegisterCalibrationCallbacks).
Una implementacin de estos manejadores o callbacks es la que se muestra en el
siguiente cdigo:
Listado 31.23: Callback para el generador de usuario
#define POSE_TO_USE "Psi"
xn::UserGenerator g_UserGenerator;
void XN_CALLBACK_TYPE
User_NewUser(xn::UserGenerator& generator,
XnUserID nId, void* pCookie){
printf("Nuevo usuario: %d\n", nId);
g_UserGenerator.GetPoseDetectionCap().
StartPoseDetection(POSE_TO_USE, nId);
}
void XN_CALLBACK_TYPE
User_LostUser(xn::UserGenerator& generator, XnUserID nId,
void* pCookie){}
void XN_CALLBACK_TYPE
Pose_Detected(xn::PoseDetectionCapability& pose,
const XnChar* strPose, XnUserID nId, void* pCookie){
printf("Pose %s para el usuario %d\n", strPose, nId);
g_UserGenerator.GetPoseDetectionCap().StopPoseDetection(nId);
g_UserGenerator.GetSkeletonCap().RequestCalibration(nId, TRUE);
}
void XN_CALLBACK_TYPE
Calibration_Start(xn::SkeletonCapability& capability, XnUserID nId,
void* pCookie){
printf("Comenzando calibracion para el usuario %d\n", nId);
}
C31
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
[1046]
29
30 void XN_CALLBACK_TYPE
31 Calibration_End(xn::SkeletonCapability& capability, XnUserID nId,
32
XnBool bSuccess, void* pCookie){
33
if (bSuccess){
34
printf("Usuario calibrado\n");
35
g_UserGenerator.GetSkeletonCap().StartTracking(nId);
36
} else {
37
printf("Fallo al calibrar al usuario %d\n", nId);
38
g_UserGenerator.GetPoseDetectionCap().
39
StartPoseDetection(POSE_TO_USE, nId);
40
}
41 }
4-10
[1047]
31.16.
Realidad Aumentada
C31
[1048]
31.16.1.
Un poco de historia
[1049]
Entorno
Real
Mundo Real
El usuario interacta
exclusivamente en el
mundo real y el
resultado tiene su
equivalencia en el
mundo virtual.
Interfaces
de Usuario
Tangibles
Entorno
Virtual
Realidad Mixta
Realidad Aumentada
Virtualidad Aumentada
Realidad
Aumentada
Espacial
Realidad Aumentada
Basada en
Dispositivos de Visin
Ejemplo de proyeccin
espacial de Realidad
Aumentada Bubble Cosmos
Realidad
Virtual
SemiInmersiva
Realidad Virtual
La Realidad Virtual se
encarga de construir
entornos totalmente
virtuales donde el usuario
interacta.
Realidad
Virtual
Inmersiva
C31
[1050]
El 2009 se presenta ARhrrrr! (ver Figura 31.27), el primer juego con contenido de
alta calidad para smartphones con cmara. El telfono utiliza la metfora de ventana
virtual para mostrar un mapa 3D donde disparar a Zombies y facilitar la salida a los
humanos que estn atrapados en l. El videojuego emplea de forma intensiva la GPU
del telfono delegando en la tarjeta todos los clculos salvo el tracking basado en
caractersticas naturales, que se realiza en la CPU. As es posible distribuir el clculo
optimizando el tiempo nal de ejecucin.
El videojuego de PSP Invizimals (ver Figura 31.28), creado por el estudio espaol
Novorama en 2009, alcanza una distribucin en Europa en el primer trimestre de
2010 superior a las 350.000 copias, y ms de 8 millones de copias a nivel mundial,
situndose en lo ms alto del rnking de ventas. Este juego emplea marcas para
registrar la posicin de la cmara empleando tracking visual.
31.16.2.
Caractersticas Generales
[1051]
La combinacin de estas tres caractersticas hacen que la Realidad Aumentada sea
muy interesante para el usuario ya que complementa y mejora su visin e interaccin
del mundo real con informacin que puede resultarle extremadamente til a la hora
de realizar ciertas tareas. De hecho la Realidad Aumentada es considerada como una
forma de Amplicacin de la Inteligencia que emplea el computador para facilitar el
trabajo al usuario.
La importancia de la Realidad Aumentada queda patente con el enorme inters
que ha generado en los ltimos meses. La prestigiosa publicacin britnicac The
Economist asegur en Septiembre de 2009 que intentar imaginar como se utilizar
la Realidad Aumentada es como intentar predecir el futuro de la tecnologa web en
1994.
Amplicacin Inteligencia
El trmino Amplicacin de la Inteligencia comenz a utilizarse desde la Introduccin a la Ciberntica de William Ross Ashby, rerindose al uso de las tecnologas
de la informacin para aumentar
la inteligencia humana. Tambin se
denomina habitualmente Aumentacin Cognitiva.
Augmented Reality
Virtual Reality
1.5
1.0
0.5
0.0
Julio 09
Octubre 09
Enero 2010
Figura 31.30: Porcentaje de bsquedas en Google por las cadenas Augmented Reality y Virtual
Reality.
31.16.3.
Alternativas tecnolgicas
C31
[1052]
31.17.
ARToolKit
31.17.1.
Instalacin y conguracin
de ARToolKit: http://www.hitlabnz.org/wiki/Forum
Figura 31.32: Aunque las bibliotecas con las que trabajaremos en este documento son libres, slo describiremos el proceso de instalacin
y conguracin bajo GNU/Linux.
Imagen original de sfgate.com.
[1053]
4: Digital Video Camcoder through IEEE 1394 (DV Format)
5: Digital Video Camera through IEEE 1394 (VGA NC Image Format)
6: GStreamer Media Framework
Enter : 3
Color conversion should use x86 assembly (not working for 64bit)?
Enter : n
Do you want to create debug symbols? (y or n)
Enter : y
Build gsub libraries with texture rectangle support? (y or n)
GL_NV_texture_rectangle is supported on most NVidia graphics cards
and on ATi Radeon and better graphics cards
Enter : y
create ./Makefile
create lib/SRC/Makefile
...
create include/AR/config.h
Done.
make. Si todo ha ido bien, ya tenemos compiladas las bibliotecas de ARToolKit. Estas
31.18.
Aunque todava quedan muchos aspectos que estudiar, comenzaremos con una
aplicacin mnima que dibuja una tetera 3D localizada en el centro de una marca de
ARToolKit. El siguiente listado muestra el cdigo completo (menos de 110 lneas!)
del Hola Mundo!. Al nal de la seccin se mostrar el listado del makefile necesario
para compilar este ejemplo.
Listado 31.24: Hola Mundo! con ARToolKit.
#include <GL/glut.h>
#include <AR/gsub.h>
#include <AR/video.h>
#include <AR/param.h>
#include <AR/ar.h>
// ==== Definicion de variables globales =========================
int
patt_id;
// Identificador unico de la marca
double patt_trans[3][4];
// Matriz de transformacion de la marca
// ======== print_error ==========================================
void print_error (char *error) { printf(error); exit(0); }
// ======== cleanup ==============================================
static void cleanup(void) {
arVideoCapStop();
// Libera recursos al salir ...
arVideoClose();
argCleanup();
}
// ======== draw =================================================
static void draw( void ) {
double gl_para[16];
// Esta matriz 4x4 es la usada por OpenGL
GLfloat mat_ambient[]
= {0.0, 0.0, 1.0, 1.0};
GLfloat light_position[] = {100.0,-200.0,200.0,0.0};
argDrawMode3D();
// Cambiamos el contexto a 3D
argDraw3dCamera(0, 0);
// Y la vista de la camara a 3D
glClear(GL_DEPTH_BUFFER_BIT); // Limpiamos buffer de profundidad
glEnable(GL_DEPTH_TEST);
C31
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
[1054]
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
}
// ======== init =================================================
static void init( void ) {
ARParam wparam, cparam; // Parametros intrinsecos de la camara
int xsize, ysize;
// Tamano del video de camara (pixels)
// Abrimos dispositivo de video
if(arVideoOpen("") < 0) exit(0);
if(arVideoInqSize(&xsize, &ysize) < 0) exit(0);
// Cargamos los parametros intrinsecos de la camara
if(arParamLoad("data/camera_para.dat", 1, &wparam) < 0)
print_error ("Error en carga de parametros de camara\n");
arParamChangeSize(&wparam, xsize, ysize, &cparam);
arInitCparam(&cparam);
// Inicializamos la camara con param"
// Cargamos la marca que vamos a reconocer en este ejemplo
if((patt_id=arLoadPatt("data/simple.patt")) < 0)
print_error ("Error en carga de patron\n");
argInit(&cparam, 1.0, 0, 0, 0, 0);
// Abrimos la ventana
}
// ======== mainLoop =============================================
static void mainLoop(void) {
ARUint8 *dataPtr;
ARMarkerInfo *marker_info;
int marker_num, j, k;
double p_width
= 120.0;
double p_center[2] = {0.0, 0.0};
[1055]
98
99
100
101
102
103
104
105
106
}
// ======== Main =================================================
int main(int argc, char **argv) {
glutInit(&argc, argv);
// Creamos la ventana OpenGL con Glut
init();
// Llamada a nuestra funcion de inicio
arVideoCapStart();
// Creamos un hilo para captura video
argMainLoop( NULL, NULL, mainLoop );
// Asociamos callbacks...
return (0);
}
94 .
C31
Uso de /dev/video*
[1056]
31.18.1.
Inicializacin
31.18.2.
Bucle Principal
De esta forma, ARToolKit nos devuelve posibles posiciones para cada una
de las marcas detectas. Incluso cuando estamos trabajando con una nica marca, es
comn que sea detectada en diferentes posiciones (por ejemplo, si hay algo parecido
a un cuadrado negro en la escena). Cmo elegimos la correcta en el caso de que
tengamos varias detecciones? ARToolKit asocia a cada percepcin una probabilidad
Figura 31.35: Ejemplo de proceso
de binarizacin de la imagen de
entrada para la deteccin de marcas.
[1057]
de que lo percibido sea una marca, en el campo cf (condence value). En la tabla 31.1
se muestra la descripcin completa de los campos de esta estructura. Como se puede
comprobar, todos los campos de la estructura ARMarkerInfo se reeren a coordenadas
2D, por lo que an no se ha calculado la posicin relativa de la marca con la cmara.
Tipo
Campo
Descripcin
int
area
int
id
int
dir
double
cf
double
pos[2]
double
line[4][3]
double
vertex[4][2]
31.18.3.
31.19.
C31
Registro Cmara 3D
Dibujado Objetos 3D
En la imagen binaria se
detectan contornos y se
extraen las aristas y las
esquinas del cuadrado.
Identificacin Patrn
Pos. + Rotacin 3D
Fuente de vdeo de
entrada.La imagen ser
binarizada a blanco y
negro para buscar marcas
cuadradas.
Posicin 2D Marcas
Bsqueda de Marcas
[1058]
Video: Este mdulo contiene las funciones para obtener frames de vdeo de los
dispositivos soportados por el Sistema Operativo. El prototipo de las funciones
de este mdulo se encuentran el chero de cabecera video.h.
AR: Este mdulo principal contiene las funciones principales de tracking de
marcas, calibracin y estructuras de datos requeridas por estos mtodos. Los
cheros de cabecera ar.h, arMulti.h (subrutinas para gestin multi-patrn)
y param.h describen las funciones asociadas a este mdulo.
Gsub y Gsub_Lite: Estos mdulos contienen las funciones relacionadas con la
etapa de representacin. Ambas utilizan GLUT, aunque la versin _Lite es
ms eciente y no tiene dependencias con ningn sistema de ventanas concreto.
En estos mdulos se describen los cheros de cabecera gsub.h, gsub_lite.h
y gsubUtil.h.
Estos mdulos estn totalmente desacoplados, de forma que es posible utilizar
ARToolKit sin necesidad de emplear los mtodos de captura de vdeo del primer
mdulo, o sin emplear los mdulos de representacin Gsub o Gsub_Lite, como
veremos en la posterior integracin con Ogre y OpenCV.
31.19.1.
Principios Bsicos
ARToolKit
Gsub
Gsub_Lite
AR Video
GLUT
OpenGL
APIs
Bibs.
Vdeo
Sistema Operativo
a) Imagen en escala de
grises.
[1059]
b) Threshold = 100
c) Threshold = 50
d) Threshold = 150
R12
R22
R32
0
R13
R23
R33
0
Xm
Tx
Xm
Ty Y m
Ym
Tz Zm = Tcm Zm
1
1
1
(31.1)
De este modo, conociendo la proyeccin de cada esquina de la marca (e) sobre las
coordenadas de pantalla (xc, yc), y las restricciones asociadas al tamao de las marcas
y su geometra cuadrada, es posible calcular la matriz Tcm . La Figura 31.40 resume
esta transformacin.
El clculo aproximado de la matriz de transformacin que representa la
rotacin R y la traslacin T desde las coordenadas de la marca a las
coordenadas de la cmara se denominan en ingls pose estimation y position
estimation.
C31
[1060]
Ym
Xm
Convenio de
ARToolKit:
el eje Z sale
desde el papel.
xc
Xc
Coordenadas
de la Cmara
Coordenadas (xce,yce)
en Pantalla
(xc, yc)
Coordenadas
de la Marca
Xm
Zm
Zc
Yc
yc
Ym
31.19.2.
Calibracin de la Cmara
carlos@kurt:calib_camera2$ ./calib_camera2
Input the distance between each marker dot, in millimeters: 40
----------Press mouse button to grab first image,
or press right mouse button or [esc] to quit.
Hecho esto nos aparecer una ventana con la imagen que percibe la cmara. Moveremos el patrn para que todos los puntos aparezcan en la imagen y presionaremos
el botn izquierdo del ratn una vez sobre la ventana para congelar la imagen. Ahora
tenemos que denir un rectngulo que rodee cada crculo del patrn (ver Figura 31.42
empleando el siguiente orden: primero el crculo ms cercano a la esquina superior
izquierda, y a continuacin los de su misma la. Luego los de la segunda la comenzando por el de la izquierda y as sucesivamente. Es decir, los crculos del patrn
deben ser recorridos en orden indicado en la Figura 31.43.
[1061]
El programa marcar una pequea cruz en el centro de cada crculo que hayamos
marcado (ver Figura 31.42), y aparecer una lnea indicando que ha sido sealada
como se muestra a continuacin. Si no aparece una cruz en rojo es porque el crculo
no se ha detectado y tendr que ser de nuevo sealado.
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Grabbed image 1.
----------Press mouse button and drag mouse to rubber-bound features (6 x 4),
or press right mouse button or [esc] to cancel rubber-bounding & retry
grabbing.
Marked feature position
1 of 24
Marked feature position
2 of 24
Marked feature position
3 of 24
...
Marked feature position 24 of 24
----------Press mouse button to save feature positions,
or press right mouse button or [esc] to discard feature positions &
retry grabbing.
Una vez que se hayan marcado los 24 puntos, se pulsa de nuevo el botn izquierdo
del ratn sobre la imagen. Esto almacenar la posicin de las marcas para la primera
imagen, y descongelar la cmara, obteniendo una salida en el terminal como la
siguiente.
### Image no.1 ###
1, 1: 239.50, 166.00
2, 1: 289.00, 167.00
...
6, 4: 514.00, 253.50
----------Press mouse button to grab next image,
or press right mouse button or [esc] to calculate distortion param.
C31
Precisin en calibracin
Como es obvio, a mayor nmero
de imgenes capturadas y marcadas, mayor precisin en el proceso
de calibracin. Normalmente con 5
6 imgenes distintas suele ser suciente.
[1062]
31.19.3.
Deteccin de Marcas
En la seccin 31.19.1 hemos visto que ARToolKit extrae los vrtices que denen
las cuatro esquinas de una marca. En la etapa de deteccin de la marca, es necesario
adems identicar qu patrn est siendo detectado. Para ello ARToolKit primero
normaliza la imagen detectada (eliminando la distorsin debida a la perspectiva)
para posteriormente comparar el patrn con las plantillas de los patrones que
puede reconocer la aplicacin (aquellas que hayan sido cargadas con la funcin
arLoadPatt).
Figura 31.45: Dos imgenes resultado del clculo del centro y el factor de distorsin.
Normalizacin
3 Resampling
4
Comparacin
con datos
de ficheros
de patrones
C31
Fichero
identic.patt
1 Identificacin
de la marca
[1063]
[1064]
31.20.
Histrico de Percepciones
Con histrico
Sin histrico
270
265
260
255
250
10
15
20
25
30
35
Figura 31.48: Comparativa de la trayectoria (en eje X) de 40 frames sosteniendo la marca manualmente
activando el uso del histrico de percepciones. Se puede comprobar cmo la grca en el caso de activar el
uso del histrico es mucho ms suave.
3
2
Error (cm)
150
125
100
75
50
25
0
Rotacin
Entre los factores que afectan a la deteccin de los patrones podemos destacar su
tamao, complejidad, orientacin relativa a la cmara y condiciones de iluminacin
de la escena (ver Figura 31.47).
0
30
45
0
-1
0
10 20 30 40 50 60
Distancia (cm)
Figura 31.47: Arriba Relacin entre el tamao de la marca y el rango mximo de deteccin efectiva.
Abajo Relacin entre el error cometido, la distancia de deteccin, y
el ngulo relativo entre la marca y
la cmara.
Histrico y precisin
La utilizacin del histrico suaviza
la captura y consigue mayor estabilidad, aunque el resultado del tracking cuenta con menor precisin.
[1065]
Como el histrico requiere como parmetro la percepcin anterior, no podr
utilizarse hasta que no dispongamos (al menos) de una percepcin. Por esta razn,
es necesario tener en cuenta este aspecto para llamar a la funcin de histrico
arGetTransMatCont (cuando tengamos una percepcin) o a la funcin sin histrico
arGetTransMat
la primera vez. Para esto, se utiliza otra variable llamada useCont
(lnea 4 ), que nos indica si ya podemos utilizar la funcin con histrico o no. Esta
variable de comportamiento
se tendr que poner a false (valor 0) cuando no
booleano
C31
[1066]
[1067]
31.21.
=================================
Identificador del patron
Es visible el objeto?
Ancho del patron
Centro del patron
Matriz asociada al patron
Puntero a funcion drawme
}
// ==== draw****** (Dibujado especifico de cada objeto) ===========
void drawteapot(void) {
GLfloat material[]
= {0.0, 0.0, 1.0, 1.0};
glMaterialfv(GL_FRONT, GL_AMBIENT, material);
glTranslatef(0.0, 0.0, 60.0);
glRotatef(90.0, 1.0, 0.0, 0.0);
glutSolidTeapot(80.0);
}
void drawcube(void) {
GLfloat material[]
= {1.0, 0.0, 0.0, 1.0};
glMaterialfv(GL_FRONT, GL_AMBIENT, material);
glTranslatef(0.0, 0.0, 40.0);
glutSolidCube(80.0);
C31
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
[1068]
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
}
// ======== cleanup ===============================================
static void cleanup(void) {
// Libera recursos al salir ...
arVideoCapStop(); arVideoClose(); argCleanup();
free(objects); // Liberamos la memoria de la lista de objetos!
exit(0);
}
// ======== draw ==================================================
void draw( void ) {
double gl_para[16];
// Esta matriz 4x4 es la usada por OpenGL
GLfloat light_position[] = {100.0,-200.0,200.0,0.0};
int i;
argDrawMode3D();
// Cambiamos el contexto a 3D
argDraw3dCamera(0, 0);
// Y la vista de la camara a 3D
glClear(GL_DEPTH_BUFFER_BIT); // Limpiamos buffer de profundidad
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
for (i=0; i<nobjects; i++) {
if (objects[i].visible) {
// Si el objeto es visible
argConvGlpara(objects[i].patt_trans, gl_para);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixd(gl_para);
// Cargamos su matriz de transf.
// La parte de iluminacion podria ser especifica de cada
// objeto, aunque en este ejemplo es general para todos.
glEnable(GL_LIGHTING); glEnable(GL_LIGHT0);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
objects[i].drawme();
// Llamamos a su funcion de dibujar
}
}
glDisable(GL_DEPTH_TEST);
// Abrimos la ventana
[1069]
}
argDrawMode2D();
argDispImage(dataPtr, 0,0);
if(k != -1) {
// Si ha detectado el patron en algun sitio...
objects[i].visible = 1;
arGetTransMat(&marker_info[k], objects[i].center,
objects[i].width, objects[i].patt_trans);
} else { objects[i].visible = 0; } // El objeto no es visible
draw();
// Dibujamos los objetos de la escena
argSwapBuffers(); // Cambiamos el buffer
La estructura de datos base del ejemplo es TObject, denida en las lneas 2-9 .
En esta estructura, el ltimo campo drawme es un puntero a funcin, que deber ser
asignado a alguna funcin que no reciba ni devuelva argumentos, y que se encargar
de dibujar el objeto.
Mantendremos en objects (lnea 11 ) una lista de objetos reconocibles en la
escena. La
para cada objeto de la lista se reservar
en la funcin addObject
memoria
(lneas 15-29 ) mediante una llamada a realloc en 21-22
. Esta memoria ser
liberada cuando nalice el programa en cleanup (lnea 49 ).
De este modo, en la funcin de inicializacin init, se llama a nuestra funcin
addObject para cada objeto que deba ser reconocido, pasndole la ruta al chero
C31
los objetos que pueden ser reconocidos por el programa 129 , y se compara el factor
de conanza de cada objeto con el que est siendo estudiado
133 .A continuacin se
obtiene la matriz de transformacin asociada al objeto en 139-140 .
la informacin de la lista de
objetos. Para cada objeto visible 65-66 se carga
su matriz
de transformacin 67-69 y se llama a su funcin propia de dibujado 75 .
[1070]
31.22.
Como hemos visto en los ejemplos de los captulos anteriores, ARToolKit calcula
la posicin de la marca en coordenadas de la cmara.
Puede entenderse como un posicionamiento de la cmara en una posicin del
espacio (correspondiente a la distancia y rotacin relativa entre la marca y la cmara
real), conservando el origen del mundo en el centro de la marca. Esta idea es
nicamente una ayuda para entender el posicionamiento real, ya que internamente
es una transformacin que se realiza en la pila ModelView, siendo equivalente mover
la cmara y mantener el objeto (la marca) esttica o a la inversa.
Y
X
Z
X
arGetT
ransM
at
Coordenadas Marca
Como vimos en el captulo de
OpenGL del mdulo 2, el sistema
de coordenadas de la marca sigue el
mismo convenio que OpenGL (ver
Figura 31.50), por lo que si utilizamos la idea intuitiva de que el origen del SRU est en la marca, podemos posicionar fcilmente los objetos con OpenGL, ya que las transformaciones se aplican siguiendo la
misma notacin.
[1071]
Y
X
d?
A-1
ns
Ma
t
ArG
Tran et
sMa
ar
Ge
tTr
a
d
SRU
Veamos a continuacin un ejemplo que trabajar con las relaciones entre la cmara
y la marca. Tener claras estas transformaciones nos permitir cambiar entre sistemas
de coordenadas y trabajar con las relaciones espaciales entre las marcas que no son
ms que transformaciones entre diferentes sistemas de coordenadas. En este programa
queremos variar la intensidad de color rojo de la tetera segn la distancia de una marca
auxiliar (ver Figura 31.51).
El problema nos pide calcular la distancia entre dos marcas. ARToolKit, mediante
la llamada a la funcin arGetTransMat nos devuelve la transformacin relativa entre
la marca y la cmara. Para cada marca podemos obtener la matriz de transformacin
relativa hacia la cmara. Podemos verlo como la transformacin que nos posiciona la
cmara en el lugar adecuado para que, dibujando los objetos en el origen del SRU,
se muestren de forma correcta. Cmo calculamos entonces la distancia entre ambas
marcas?
En el diagrama de la Figura 31.53 se resume el proceso. Con la llamada a
de cada cmara (que, en el caso de una nica marca, podemos verlo como si fuera el
origen del SRU donde dibujaremos los objetos). En este caso, tenemos las echas que
parten de la marca y posicionan relativamente la cmara sealadas con A y B.
C31
[1072]
X
Y
C-1
Y
Figura 31.52: Esquema de posicionamiento global utilizando transformaciones inversas.
}
}
glDisable(GL_DEPTH_TEST);
// Abrimos la ventana
Posicionamiento Global
El uso de transformaciones inversas
nos permiten obtener coordenadas
de posicionamiento global, como se
muestra en la Figura 31.52. En ese
ejemplo, bastar con conocer la matriz de transformacin entre la marca y el origen del SRU (expresada
como M ), para obtener el posiconamiento global de la cmara respecto del SRU, multiplicando la inversa de la transformacin relativa
de la marca C 1 con la matriz M .
[1073]
31.23.
C31
[1074]
cin de los frames (ver lneas 11-12 ), que pueden ser convertidos entre s fcilmente.
El IplImage ha sido descrito anteriormente en la seccin. cv::Mat es una extensin
para trabajar con matrices de cualquier tamao y tipo en C++, que permiten acceder
cmodamente a los datos de la imagen. De este modo, cuando se capture un frame, el
VideoManager actualizar los punteros anteriores de manera inmediata.
Listado 31.28: VideoManager.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include
#include
#include
#include
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 "VideoManager.h"
VideoManager::VideoManager(int device, int w, int h,
Ogre::SceneManager* sm){
_sceneManager = sm;
_capture = cvCreateCameraCapture(device);
cvSetCaptureProperty(_capture, CV_CAP_PROP_FRAME_WIDTH, w);
cvSetCaptureProperty(_capture, CV_CAP_PROP_FRAME_HEIGHT, h);
createBackground (w, h); _frameIpl = NULL; _frameMat = NULL;
}
<Ogre.h>
<iostream>
"cv.h"
"highgui.h"
class VideoManager {
private:
void createBackground(int cols, int rows);
void ReleaseCapture();
CvCapture* _capture;
IplImage* _frameIpl;
cv::Mat* _frameMat;
Ogre::SceneManager* _sceneManager;
public:
VideoManager(int device, int w, int h,
Ogre::SceneManager* sm);
~VideoManager();
void UpdateFrame();
IplImage* getCurrentFrameIpl();
cv::Mat* getCurrentFrameMat();
void DrawCurrentFrame();
};
El constructor de la clase (lneas 4-8 del siguiente listado) se encarga de obtener
el dispositivo de captura e inicializarlo con las dimensiones especicadas como
parmetro al constructor. Esto es necesario, ya que las cmaras generalmente permiten
trabajar con varias resoluciones.
En el destructor se libera el dispositivo de captura
creado por OpenCV (lnea 12 ).
Listado 31.29: VideoManager.cpp
VideoManager::~VideoManager(){
cvReleaseCapture(&_capture); delete _frameIpl; delete _frameMat;
}
// ================================================================
// createBackground: Crea el plano sobre el que dibuja el video
void VideoManager::createBackground(int cols, int rows){
Ogre::TexturePtr texture=Ogre::TextureManager::getSingleton().
createManual("BackgroundTex", // Nombre de la textura
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
Ogre::TEX_TYPE_2D,
// Tipo de la textura
cols, rows, 0,
// Filas, columas y Numero de Mipmaps
Ogre::PF_BYTE_BGRA,
Ogre::HardwareBuffer::HBU_DYNAMIC_WRITE_ONLY_DISCARDABLE);
[1075]
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().
create("Background",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
mat->getTechnique(0)->getPass(0)->createTextureUnitState();
mat->getTechnique(0)->getPass(0)->setDepthCheckEnabled(false);
mat->getTechnique(0)->getPass(0)->setDepthWriteEnabled(false);
mat->getTechnique(0)->getPass(0)->setLightingEnabled(false);
mat->getTechnique(0)->getPass(0)->getTextureUnitState(0)->
setTextureName("BackgroundTex");
// Creamos un rectangulo que cubra toda la pantalla
Ogre::Rectangle2D* rect = new Ogre::Rectangle2D(true);
rect->setCorners(-1.0, 1.0, 1.0, -1.0);
rect->setMaterial("Background");
// Dibujamos el background antes que nada
rect->setRenderQueueGroup(Ogre::RENDER_QUEUE_BACKGROUND);
Ogre::SceneNode* node = _sceneManager->getRootSceneNode()->
createChildSceneNode("BackgroundNode");
node->attachObject(rect);
}
// ================================================================
// UpdateFrame: Actualiza los punteros de frame Ipl y frame Mat
void VideoManager::UpdateFrame(){
_frameIpl = cvQueryFrame(_capture);
_frameMat = new cv::Mat(_frameIpl);
}
// = IplImage* getCurrentFrameIpl =================================
IplImage* VideoManager::getCurrentFrameIpl(){ return _frameIpl; }
// = IplImage* getCurrentFrameMat =================================
cv::Mat* VideoManager::getCurrentFrameMat(){ return _frameMat; }
// ================================================================
// DrawCurrentFrame: Despliega el ultimo frame actualizado
void VideoManager::DrawCurrentFrame(){
if(_frameMat->rows==0) return;
Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().
getByName("BackgroundTex",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
Ogre::HardwarePixelBufferSharedPtr pBuffer = tex->getBuffer();
pBuffer->lock(Ogre::HardwareBuffer::HBL_DISCARD);
const Ogre::PixelBox& pixelBox = pBuffer->getCurrentLock();
C31
El mtodo createBackground se encarga de crear un plano sobre el que se actualizar el vdeo obtenido de la cmara. Adems de las propiedades
relativas a desactivar
[1076]
rectngulo 2D (coordenadas paramtricas
en la lnea 37 ), que aadiremos al grupo
que se dibujar primero (lnea
41). Finalmente se aade el nodo, con el plano y la
textura, a la escena (lneas 43-45 ) empleando el puntero al SceneManager indicado
en el constructor.
La actualizacin de la textura asociada al plano se realiza en el mtodo DrawCurrentFrame, que se encarga de acceder a nivel de pxel y copiar el valor de la imagen
obtenida
buffer (l por la webcam. Para ello se obtiene un puntero compartido al pixel
nea 64 ), y obtiene el uso de la memoriacon exclusin mutua (lnea 67 ). Esa regin
ser posteriormente liberada en la lnea 79 . La actualizacin
de la regin se realiza
#include
#include
#include
#include
#include
#include
<AR/ar.h>
<AR/gsub.h>
<AR/param.h>
<Ogre.h>
<iostream>
"cv.h"
class ARTKDetector {
private:
ARMarkerInfo *_markerInfo;
int _markerNum;
int _thres;
int _pattId;
int _width; int _height;
double _pattTrans[3][4];
bool _detected;
int readConfiguration();
int readPatterns();
void Gl2Mat(double *gl, Ogre::Matrix4 &mat);
public:
ARTKDetector(int w, int h, int thres);
~ARTKDetector();
bool detectMark(cv::Mat* frame);
void getPosRot(Ogre::Vector3 &pos, Ogre::Vector3 &look,
Ogre::Vector3 &up);
};
En este sencillo ejemplo nicamente se han creado dos clases para la deteccin de
la marca detectMark, y para obtener los vectores de posicionamiento de la cmara en
funcin de la ltima marca detectada (getPosRot).
Listado 31.31: ARTKDetector.cpp
1
2
3
4
5
6
7
8
9
10
#include "ARTKDetector.h"
ARTKDetector::ARTKDetector(int w, int h, int thres){
_markerNum=0;
_markerInfo=NULL; _thres = thres;
_width = w;
_height = h;
_detected = false;
readConfiguration();
readPatterns();
}
ARTKDetector::~ARTKDetector(){ argCleanup(); }
// ================================================================
Compltame!
Sera conveniente aadir nuevos
mtodos a la clase ARTKDetector,
que utilice una clase que encapsule
los marcadores con su ancho, alto,
identicador de patrn, etc...
[1077]
// readPatterns: Carga la definicion de patrones
int ARTKDetector::readPatterns(){
if((_pattId=arLoadPatt("data/simple.patt")) < 0) return -1;
return 0;
}
// ================================================================
// readConfiguration: Carga archivos de configuracion de camara...
int ARTKDetector::readConfiguration(){
ARParam wparam, cparam;
// Cargamos los parametros intrinsecos de la camara
if(arParamLoad("data/camera_para.dat",1, &wparam) < 0) return -1;
arParamChangeSize(&wparam, _width, _height, &cparam);
arInitCparam(&cparam);
// Inicializamos la camara con param"
return 0;
}
// ================================================================
// detectMark (FIXME): Ojo solo soporta una marca de tamano fijo!
bool ARTKDetector::detectMark(cv::Mat* frame) {
int j, k;
double p_width
= 120.0;
// Ancho de marca... FIJO!
double p_center[2] = {0.0, 0.0};
// Centro de marca.. FIJO!
_detected = false;
if(frame->rows==0) return _detected;
if(arDetectMarker(frame->data, _thres,
&_markerInfo, &_markerNum) < 0){
return _detected;
}
for(j=0, k=-1; j < _markerNum; j++) {
if(_pattId == _markerInfo[j].id) {
if (k == -1) k = j;
else if(_markerInfo[k].cf < _markerInfo[j].cf) k = j;
}
}
if(k != -1) {
// Si ha detectado el patron en algun sitio...
arGetTransMat(&_markerInfo[k], p_center, p_width, _pattTrans);
_detected = true;
}
return _detected;
}
// ================================================================
// Gl2Mat: Utilidad para convertir entre matriz OpenGL y Matrix4
void ARTKDetector::Gl2Mat(double *gl, Ogre::Matrix4 &mat){
for(int i=0;i<4;i++) for(int j=0;j<4;j++) mat[i][j]=gl[i*4+j];
}
// ================================================================
// getPosRot: Obtiene posicion y rotacion para la camara
void ARTKDetector::getPosRot(Ogre::Vector3 &pos,
Ogre::Vector3 &look, Ogre::Vector3 &up){
if (!_detected) return;
double glAuxd[16]; Ogre::Matrix4 m;
argConvGlpara(_pattTrans,glAuxd);
Gl2Mat(glAuxd, m);
// Convertir a Matrix4 de Ogre
C31
Esta clase est preparada para trabajar con una nica marca (cargada en el mtodo
privado readPatterns), consus variables
asociadas denidas como miembros de la cla
se ARTKDetector (lneas 11-16 ). El siguiente listado implementa el ARTKDetector.
[1078]
15-16
).
La
deteccin de la marca se realiza pasndole el puntero de tipo Mat del
#include "MyFrameListener.h"
MyFrameListener::MyFrameListener(Ogre::RenderWindow* win,
Ogre::Camera* cam, Ogre::SceneNode *node,
Ogre::OverlayManager *om, Ogre::SceneManager *sm) {
// Omitido el resto del codigo...
_videoManager = new VideoManager(1, 640, 480, _sceneManager);
_arDetector = new ARTKDetector(640, 480, 100);
}
bool MyFrameListener::frameStarted(const Ogre::FrameEvent& evt) {
// Omitido el resto del codigo...
Ogre::Vector3 pos; Ogre::Vector3 look;
Ogre::Vector3 up;
18
19
20
21
22
23 }
_videoManager->UpdateFrame();
_videoManager->DrawCurrentFrame();
if (_arDetector->detectMark(_videoManager->getCurrentFrameMat()))
{
_arDetector->getPosRot(pos, look, up);
_camera->setPosition(pos);
_camera->lookAt(look);
_camera->setFixedYawAxis(true, up);
}
31.24.
Consideraciones Finales
Como no poda ser de otra forma, se han quedado muchas cosas en el tintero
relativas al uso de ARToolKit, como el uso de patrones multimarca, o el trabajo
exhaustivo con cambios entre sistemas de coordenadas. Se recomienda como ejercicio
ampliar la funcionalidad de la clase ARTKDetector para que soporte el uso del
histrico de percepciones y la carga de mltiples marcas.
Anexos
Anexo
Introduccin a OgreFramework
Sergio Fernndez Durn
ada vez que creamos un nuevo proyecto con Ogre, existe un grupo de elementos
que necesitamos instanciar para crear un primer prototipo. Este conjunto de
clases y objetos se repiten con frecuencia para todos nuestros proyectos. En
este captulo estudiaremos los fundamentos de OgreFramework, que ofrece una serie
de facilidades muy interesantes.
A.1.
Introduccin
[1082]
Existen dos ramas del framework para Ogre, Basic y Advanced. La diferencia
entre los dos es la cantidad de recursos que manejan. Basic OgreFramework contiene
los recursos mnimos necesarios para crear una instancia del motor Ogre y que
muestre una escena como ejemplo. Y Advanced OgreFramework incluye un conjunto
completo de herramientas y clases para una gestin avanzada de nuestra aplicacin.
OgreFramework est desarrollado en C++ sobre Ogre y es multiplataforma, sin
embargo, es necesario realizar algunas modicaciones en el framework para que pueda
funcionar en OSX. Una de las mayores ventajas de utilizar esta tecnologa es la de
poder automatizar la carga inicial del motor y poder congurar Ogre con llamadas
simples a OgreFramework.
A.2.
Basic OgreFramework
Una de las mejores formas de iniciarse con Ogre es mediante esta tecnologa. Con
esta herramienta no es necesario conocer la arquitectura del motor para poder crear un
primer ejecutable y ayuda a conocer cuales son sus funciones bsicas. Este paquete de
herramientas incluye:
Inicializacin de Ogre.
Bucle de renderizado personalizable.
Escena bsica.
Gestin de teclado bsica.
Crear capturas de pantallas (Screenshots).
Informacin bsica de la escena (FPS, contador batch. . . ).
Para conseguir la ltima versin de Basic OgreFramework clonaremos el proyecto
con mercurial desde la siguiente direccin:
hg clone https://bitbucket.org/spacegaier/basicogreframework
A.2.1.
Arquitectura
[1083]
Todos estos gestores de la clase OgreFramework sern explicados con ms detalle
en la siguiente seccin, donde hablaremos de la rama Advanced. Para poder crear un
ejemplo basado en Basic OgreFramework es necesario:
Llamar a las funciones de inicio, conguracin y ejecucin del motor.
Un puntero a una instancia de OgreFramework.
Una entidad y un nodo escena.
Una variable booleana que gestione el bucle principal del juego.
Ahora veremos los pasos necesarios para rellenar de contenido nuestra escena.
Para ello crearemos nodos y escenas igual que las creamos en Ogre pero llamando al
puntero de OgreFramework, como se muestra en el siguiente listado.
Listado A.1: Creacin de escena.
1 void ExampleApp::createScene()
2 {
3
OgreFramework::getSingletonPtr()->
4
m_pSceneMgr->setSkyBox(true, "Examples/Sky");
5
OgreFramework::getSingletonPtr()->
6
m_pSceneMgr->createLight("Light")->setPosition(75,75,75);
7
8
m_pSinbadEntity = OgreFramework::getSingletonPtr()->
9
m_pSceneMgr->createEntity("SinbadEntity", "Sinbad.mesh");
10
m_pSinbadNode = OgreFramework::getSingletonPtr()->
11
m_pSceneMgr->getRootSceneNode()->createChildSceneNode("
SinbadNode");
12
m_pSinbadNode->attachObject(m_pSinbadEntity);
13 }
Como podemos observar, para crear una entidad y un nodo, es necesario acceder
al gestor de escena mediante el puntero Singleton a OgreFramework. Se realizar
la misma accin para crear las luces de la escena y el Skybox. Esta restriccin
nos ayudar a mantener separado el motor Ogre de nuestro modelo y permitir la
integracin con otros mdulos de forma ms homognea para el desarrollador.
Realmente, cuando accedamos a uno de los gestores de OgreFramework como el
SceneManager, utilizaremos las mismas primitivas que utilizabamos en Ogre. Por lo
tanto, podremos usar la documentacin estndar de Ogre.
Para gestionar nuestro bucle del juego necesitaremos la variable booleana m_bShutdown
la cual nos permitir cerrar la aplicacin en cualquier momento. Si se produce un error
inesperado, tambin saldr del bucle principal.
En cada iteracin del bucle:
Se capturan las rdenes introducidas a travs de teclado y ratn para mostrar
nuevos eventos en la siguiente iteracin.
Se guarda el tiempo transcurrido desde el inicio.
Se actualiza el motor Ogre con el tiempo del ltimo frame generado.
Se renderiza un frame.
Se calcula el tiempo transcurrido desde el ltimo frame generado.
[1084]
A.3.
Advanced OgreFramework
Para poder realizar un proyecto ms complejo en Ogre, este framework nos sirve
como ncleo. Este paquete de herramientas incluye lo siguiente:
Inicializacin de OGRE.
Carga de recursos bsicos.
Gestin de dispositivos de entrada basado en OIS.
Personalizacin del bucle de renderizado.
Jerarqua basada en estados.
Cargador de escenas mediante XML con DotSceneLoader.
Interfaz grca de usuario basada en SdkTrays.
Manipulacin de materiales en cdigo.
Para conseguir la ltima versin de Advanced OgreFramework clonaremos el
proyecto con mercurial desde la siguiente direccin:
hg clone https://bitbucket.org/spacegaier/advancedogreframework
En este ejemplo, todos los recursos de la escena han sido cargados desde un XML
con la herramienta DotSceneLoader. Este chero contiene las propiedades de cada
nodo y se carga de manera dinmica en nuestra escena.
A.3.1.
Sistema de Estados
Cada estado creado en nuestra aplicacin incluye las siguientes acciones: entrar,
pausar, resumir y salir. Para que todos los estados mantengan esta condicin se
utiliza herencia de la clase AppState. La clase que gestiona todos los estados es
AppStateManager cuya tarea es manejarlos y conectarlos. Para hacer esto, el manager
[1085]
tiene una pila de estados activos y siempre ejecuta el estado de la cima de la pila. En
cualquier momento, se puede conectar otro estado de la pila, que ser usado hasta que
sea eliminado de la misma. En este caso, el manager resume el estado que dej en
pausa al introducir el anterior estado eliminado. En el momento en que la aplicacin
no contiene ningn estado activo, el manager cierra la aplicacin.
A.3.2.
Arquitectura
[1086]
Podemos ver que para crear una escena es necesario introducir 4 argumentos: el
chero, el nombre del nodo, el SceneManager que se est gestionando en este estado
y el nodo donde queremos introducirlo. Es muy interesante poder cargar las escenas
desde un chero XML por que podremos crearlas y destruirlas de manera dinmica,
permitiendo una mejor gestin de la memoria y del nivel del juego.
Normalmente, se pueden agrupar las escenas de un nivel por bloques e ir cargando
cada uno cuando sea necesario. Si, por ejemplo, nos encontramos con un juego
tipo endless-running, es interesante ir destruyendo las escenas que quedan detrs
del personaje ya que no vam a ser accesibles; tambin es interesante para ir autogenerando la escena de forma aleatoria hacia adelante.
Cada vez que carguemos una escena, podremos recuperar una entidad como se
describe en el ejemplo; por un nombre bien conocido. Este nombre estar asignado en
el chero XML y podremos recuperar el nodo desde el propio juego. De esta manera
podremos cambiar las propiedades o materiales de dicha entidad al vuelo como, por
ejemplo, aadir un cuerpo rgido al motor de fsicas y conectarlo a la entidad.
[1087]
AppState
En este archivo se denen dos clases. La primera clase ser la que herede el
AppStateManager que gestiona los estados, pero est denido en esta clase por
cuestiones de diseo. Esta contiene los mtodos necesarios para la gestin de los
estados, estos mtodos son abstractos y estn denidos en la clase AppStateManager.
La segunda clase ser AppState la cul ser la clase de la que hereden todos los
estados que se implementen en el juego. Esta clase contiene una serie de mtodos
para mantener la consistencia entre estados y poder tratar cada uno como estado
independiente. En l se incluyen los mtodos: enter, exit, pause, resume y update.
Estos mtodos nos permitirn sobre todo mantener la interfaz organizada en cada
estado como, por ejemplo, el track de msica que se est escuchando en cada estado.
Cuando creamos un estado nuevo y lo agregamos a la pila de estados.
AppStateManager
Esta clase ser la encargada de gestionar todos los estados. Tendr los mecanismos
para pausar, recuperar y cambiar un estado en cualquier instante del juego. Hereda
directamente del AppStateListener e implementa sus mtodos abstractos. Contiene
dos vectores:
Un vector para todos los estados existentes.
Un vector para los estados activos.
Para cada estado existirn una serie de valores como su nombre, informacin y su
estado. Cada vez que se cree un estado, se llamar al mtodo manageAppState() para
que introduzca dicha informacin al estado y lo inserte en la pila de estados existentes.
En esta clase se encuentra el bucle principal del juego, dentro del mtodo start().
Dentro de este mtodo podemos destacar el clculo del tiempo de un frame a otro.
Aqu podremos realizar las modicaciones necesarias si necesitamos coordinar el
motor de renderizado con el motor de fsicas. Para poder arreglar el problema en
el desfase de tiempo entre ambos motores se ajusta un valor delta que discretiza el
tiempo entre un frame y otro. Esta tcnica nos ayudar a manejar correctamente el
tiempo en ambos motores para que se comporte de manera natural.
Listado A.3: Bucle principal del juego.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
double t = 0.0;
const double dt = 0.01;
double currentTime = tiempo_actual_en_segundos();
double accumulator = 0.0;
while ( !quit ) {
double newTime = time(); // Tiempo actual en segundos
double frameTime = newTime - currentTime; // Tiempo de un frame
if ( frameTime > 0.25 )
frameTime = 0.25;
// Tiempo maximo de espera entre frames
currentTime = newTime;
accumulator += frameTime;
while ( accumulator >= dt ) {
updateOgre(dt);
accumulator -= dt;
}
[1088]
MenuState
La clase MenuState ser una de las implementaciones de la clase AppState.Por
herencia se asegura que todos los estados tienen los mismos mtodos comunes
y funciones como enter(), exit(), pause(), resume() y update(). Estos mtodos se
ejecutarn automticamente por el AppStateManager. Esto es, si el estado actual
fue pausado, ejecutar el mtodo resume y seguir la ejecucin del bucle principal.
Igual para el resto de estados. En todos los estados se escribir la macro DECLARE_APPSTATE_CLASS(MenuState) que insertar el nombre del estado para poder ser
recuperado posteriormente.
Dentro de la funcin enter() de MenuState se crea un SceneManager, se crea la
cmara, se construye la interfaz grca de usuario y se llama al creador de escena. En
este caso el creador de escena no tendr nada puesto que ser un estado en el que se
base en la interfaz grca creada por SdkTrays y no en Ogre.
En el mtodo exit() estarn todos los destructores de escena, interfaz y cmara.
De esta forma aseguramos la integridad entre estados. Los manejadores de entrada
(teclado/ratn) tambin estarn presentes en todos los estados y servirn para capturar
los eventos que se realicen en la interfaz.
La funcin update() actualiza los elementos de la interfaz como, por ejemplo, si el
ratn est encima de un botn cambia la imagen; si, por otro lado, se pulsa el botn,
la imagen cambia de nuevo. Adems este mtodo capturar si se ha enviado la orden
de salir del sistema para nalizar la aplicacin.
Y por ltimo, el mtodo buttonHit() es un callback que se accionar cuando se
pulse un botn. Dentro de este mtodo podremos identicar que botn se ha accionado
y realizar una accin consecuente con ello.
GameState
Toda la lgica del juego est dentro del estado GameState. Aqu es donde
enlazaremos todos nuestros elementos de nuestro juego, como por ejemplo: el
motor de fsicas, el controlador de nuestro personaje principal, mdulos de inteligencia
articial, etc. En el caso de ejemplo de OgreFramework, incluye una escena mnima
que carga una escena con DotSceneLoader y rellena de contenido la pantalla.
Para crear los elementos necesarios de la interfaz se invoca a builGUI() para insertar todos los elementos necesarios del estado mediante el gestor SdkTraysManager
que explicaremos en la siguiente seccin.
Para capturar los eventos de entrada por teclado, se utilizarn los mtodos
keyPressed() y keyReleased() para realizar los eventos correspondientes a si se pulsa
una tecla o se ha dejado de pulsar respectivamente. En nuestro caso, tendremos una
serie de condiciones para cada tecla que se pulse, las teclas son capturadas mediante
las macros denidas en la librera OIS.
A.4.
SdkTrays
Tanto los ejemplos que vienen con el SDK de Ogre (SampleBrowser), como
OgreFramework utilizan SdkTrays. Este gestor de la interfaz se construy con el
propsito de ser sencillo, crear interfaces de una manera rpida y sin tanta complejidad
como CEGUI. El sistema de SdkTrays est basado en Ogre::Overlay por lo que nos
garantiza su portabilidad a todas las plataformas.
A.4. SdkTrays
[1089]
A.4.1.
Introduccin
A.4.2.
Requisitos
A.4.3.
SdkTrayManager
A.4.4.
Widgets
A.4.5.
SdkTrayListener
Esta clase contiene los controladores para todos los diferentes eventos que pueden
disparar los widgets. La clase SdkTrayManager es, en s misma, un SdkTrayListener
porque responde a los eventos de los widgets. Algunos widgets dan la opcin de no
disparar un evento cuando cambia su estado. Esto es til para inicializar o restablecer
un widget, en cuyo caso no debe haber una respuesta de cualquier tipo.
[1090]
A.4.6.
Skins
A.4.7.
OgreBites
A.5.
btOgre
A.5. btOgre
[1091]
2
3
4
5
6
7
8
9
BtOgre::DebugDrawer m_pDebugDrawer =
new BtOgre::DebugDrawer(OgreFramework::getSingletonPtr()->
m_pSceneMgr->getRootSceneNode(), m_pDynamicsWorld);
m_pDynamicsWorld->setDebugDrawer(m_pDebugDrawer);
// Actualizamos el motor Bullet
m_pDynamicsWorld->stepSimulation(timeSinceLastFrame);
m_pDebugDrawer->step();
[1092]
");
m_pSphereNode->attachObject(m_pSphereEntity);
//Creamos la forma de la malla en Bullet.
BtOgre::StaticMeshToShapeConverter converter(m_pSphereEntity);
m_pSphereShape = converter.createSphere();
// Insertamos la masa al cuerpo rigido.
btScalar mass = 5;
btVector3 inertia;
m_pSphereShape->calculateLocalInertia(mass, inertia);
// Creamos un estado de BtOgre (conectamos Ogre y Bullet).
BtOgre::RigidBodyState *state = new BtOgre::RigidBodyState(
m_pSphereNode);
// Creamos el Body.
BtOgre::btRigidBody m_pRigidBody =
new btRigidBody(mass, state, m_pSphereShape, inertia);
// Mundo fisico de Bullet (btDynamicsWorld).
m_pDynamicsWorld->addRigidBody(m_pRigidBody);
A.6.
Referencias
http://www.ogre3d.org/tikiwiki/Basic+Ogre+Framework
http://www.ogre3d.org/tikiwiki/Advanced+Ogre+Framework
http://bitbucket.org/spacegaier/basicogreframework
http://bitbucket.org/spacegaier/advancedogreframework
http://gafferongames.com/game-physics/fix-your-timestep/
http://ogrees.wikispaces.com/Tutorial+Framework+Avanzado+
de+Ogre
http://www.ogre3d.org/tikiwiki/SdkTrays
http://www.ogre3d.org/tikiwiki/OgreBites
http://github.com/nikki93/btogre
Anexo
n este anexo se explicarn ciertos detalles del juego Vault Defense. Detalles de
su GUI (Graphical User Interface), interfaz que usa CEGUI y con el cual, se
han creado unos botones propios, una pantalla de carga y un efecto de cmara
manchada de sangre, tambin se explicar al detalle el efecto de parpadeo de las luces,
usado para darles un aspecto de luz de antorcha, y por ltimo los enemigos, unos
agentes de estado que implementan ciertos comportamientos.
Pero antes de explicar dichos detalles del juego, vamos a explicar cmo congurar
Visual C++ 2010 Express 1 para poder compilar nuestros juegos con OGRE + CEGUI
para la plataforma Windows.
B.1.
1093
[1094]
B.1.1.
-----------
-- Renderers
-- this controls which renderer modules are built
En este apartado del archivo podemos congurar qu proyectos queremos crear
para Visual C++. Para ello, pondremos a true la variable OGRE_RENDERER .
Con esto, el archivo estar bien congurado para nuestras necesidades, y aunque se
puede congurar mejor para que ponga las rutas bien desde el principio es mejor
aprender dnde se congura el proyecto en el programa. Ahora ejecutaremos el
archivo build_vs2008.bat , aunque es un proyecto de 2008 se podr cargar con Visual
C++ 2010, se generar un archivo CEGUI.sln, dicho archivo ser abierto con el
Visual C++. cuando lo abramos, aceptaremos la conversin del proyecto sin tocar
las opciones y ya podremos pasar a congurar las rutas.
Con el proyecto ya cargado y con la conversin ya realizada podemos pasar
a congurar las rutas. Lo primero es saber que Visual C++ mantiene 2 tipos de
conguraciones: una para modo debug y otra para modo Release, se pueden congurar
muchas ms, pero no ser necesario. Comprobamos si estn bien las rutas, que tienen
que estarlo en el proyecto CEGUIBase, pulsaremos botn derecho/propiedades sobre
el proyecto, en el explorador de soluciones y nos saldr una ventana de conguracin
(ver gura B.1), o con el proyecto seleccionado Alt + Enter. Observamos que esta
activo el modo Release, tambin har falta comprobar las rutas para el modo Debug.
Esto es lo que veremos:
1. Apartado C/C++ / general (Figura B.2). En dicho apartado se conguran los includes del proyecto, podemos observar que en CEGUIBase se aaden ../.
./../cegui/include; y, por otra parte, ../../../dependencies/
incluye; por eso es necesario colocar la carpeta dependencias en la carpeta
de CEGUI.
2. Vinculador / General (Figura B.3). Aqu es donde diremos dnde estn nuestras
bibliotecas (las carpetas Lib).
2 http://www.cegui.org.uk/wiki/index.php/Downloads
3 http://www.ogre3d.org/download/sdk
[1095]
3. Vinculador / Entrada (Figura B.4). En este apartado se colocan los nombre de las
bibliotecas adicionales, es decir, las bibliotecas que estn en las rutas aadidas
en general y que vayamos a usar.
Ahora lo siguiente es comprobar que CEGUIOgreRenderer est bien congurado repasando dichos apartados. Por defecto el cong.lua tiene la ruta de Ogre pero
mal puesta, tendremos que congurar todos los apartados. Al nal quedar algo as,
suponiendo que lo tenis instalado en la raz.
Con las rutas bien conguradas pasamos a compilar toda la solucin, esto creara
una serie de archivos en la carpeta CEGUI-0.7.9/lib y CEGUI-0.7.9/bin, algunos de
estos archivos seran necesarios para poder compilar nuestros juegos.
B.1.2.
[1096]
[1097]
[1098]
Plugin=Plugin_ParticleFX_d
Ya con estos 3 archivos aadidos solo falta crear una carpeta plugins y aadir los
.dlls correspondientes dentro de ella.
Con los plugins colocados pasamos a aadir los .dlls que faltan. Estos varan segn
las bibliotecas usadas pero por lo general har falta OgreMain y OIS. Acordaros de
copiar los dlls correspondientes segn vuestra compilacin, si estis en modo debug
usad los que terminan en _d.
[1099]
[1100]
B.2.
En Vault Defense se han creado unos botones propios, una pantalla de carga y un
efecto de salpicadura en cmara, todo esto realizado con CEGUI.
B.2.1.
Botones propios
[1101]
44
</DimOperator>
45
</UnifiedDim>
46
</Dim>
47
<Dim type="Height"><UnifiedDim scale="1"
48
type="Height" /></Dim>
49
</Area>
50
<Image imageset="VaultDefense"
51
image="BotonSinglePlayerNormal" />
52
<VertFormat type="Stretched" />
53
<HorzFormat type="Stretched" />
54
</ImageryComponent>
55
</ImagerySection>
56
57
<ImagerySection name="hover">
58
<!-.................... -->
59
</ImagerySection>
60
61
<ImagerySection name="pushed">
62
<!-.................... -->
63
</ImagerySection>
64
65
<ImagerySection name="disabled">
66
<!-.................... -->
67
</ImagerySection>
68
<StateImagery name="Normal">
69
<Layer>
70
<Section section="normal" />
71
</Layer>
72
</StateImagery>
73
<StateImagery name="Hover">
74
<Layer>
75
<Section section="hover" />
76
</Layer>
77
</StateImagery>
78
<StateImagery name="Pushed">
79
<Layer>
80
<Section section="pushed" />
81
</Layer>
82
</StateImagery>
83
<StateImagery name="PushedOff">
84
<Layer>
85
<Section section="hover" />
86
</Layer>
87
</StateImagery>
88
<StateImagery name="Disabled">
89
<Layer>
90
<Section section="disabled">
91
<Colours topLeft="FF7F7F7F" topRight="FF7F7F7F"
92
bottomLeft="FF7F7F7F" bottomRight="FF7F7F7F" />
93
</Section>
94
</Layer>
95
</StateImagery>
96 </WidgetLook>
[1102]
StaticImage"
22
LookNFeel="VaultDefense/StaticImage" />
23 </GUIScheme>
Denimos que imageset vamos a usar, que looknfeel usamos, y luego pasamos a
crear los botones con:
Listado B.3: Creacin de botones.
1 <FalagardMapping WindowType="VaultDefense/BotonCreditos"
2
TargetType="CEGUI/PushButton" Renderer="Falagard/Button"
3
LookNFeel="VaultDefense/BotonCreditos"/>
[1103]
En este cdigo podemos ver la creacin de unos botones muy simples, se ha cogido
como base un archivo scheme ya creado y se ha modicado. En WindowType ponemos
el identicador del window para poder usarlo en nuestros layouts y en LookNFeel
ponemos nuestro lookNFeel modicado.
En el cdigo es necesario cargar nuestros propios archivos:
Listado B.4: Carga de archivos.
1 CEGUI::SchemeManager::getSingleton().create
2
("VaultDefense.scheme");
3
4 CEGUI::ImagesetManager::getSingleton().create
5
("VaultDefense.imageset");
B.2.2.
Pantalla de Carga
[1104]
Este mtodo de carga no calcula el tiempo real de lo que tardan las operaciones,
es una carga por pasos. Se denen una serie de cargas, y segun se van cargando se
va pasando de paso. En resumen, se dividen los calculos en grupos y se realiza un
RenderOneFrame por cada calculo realizado, cuando se realiza el calculo se ejecuta
este cdigo.
Listado B.6: Renderizando frame a frame.
1 progressBar->step();
2 staticText->setText("Calculando caminos 2-5");
3 _root->renderOneFrame();
Esto se puede cambiar, se puede crear una barra propia y hacerle un escalado.
Tambin se puede poner una imagen en el centro e ir cambindola. Existen muchas
maneras, pero sta es la ms sencilla.
B.2.3.
Manchas en la cmara
[1105]
2
3
4
5
6
7
8
9
Name="UI/Fondo/Sangre10"><Property Name="Image"
Value="set:VaultDefenseSangre2 image:Sangre"/>
<Property Name="UnifiedAreaRect"
Value="{{0.0,0},{0.0,0},{1,0},{1,0}}"/>
<Property Name="BackgroundEnabled" Value="False"/>
<Property Name="Alpha" Value="0"/>
<Property Name="FrameEnabled" Value="False"/>
</Window>
Luego en alguna parte del juego, en el caso de Vault Defense cuando el personaje
recibe un impacto, activamos una de las sangres de manera aleatoria, mostrndola y
poniendo su alpha a 1, durante el enterFrame() vamos decrementando el alpha.
Listado B.11: Activando la sangre.
1 for(int i = 0; i < _sangrado.size(); i++) {
2
3
if(_sangrado[i].activo) {
4
CEGUI::DefaultWindow* staticImage =
5
static_cast<CEGUI::DefaultWindow*>
6
(CEGUI::WindowManager::getSingleton().
7
getWindow(_sangrado[i].nombre));
8
9
_sangrado[i].alpha-=0.5*deltaT;
10
11
std::stringstream auxS;
12
auxS << _sangrado[i].alpha;
13
staticImage->setProperty("Alpha",auxS.str());
14
15
if(_sangrado[i].alpha<=0.0) {
16
_sangrado[i].activo=false;
17
}
18
19
}
20
21 }
[1106]
B.3.
[1107]
B.4.
_ia = sceneMgr->createBillboardSet(billAux.str(),1);
_ia->setBillboardType(Ogre::BBT_POINT);
_ia->setMaterialName("IATesoro");
_ia->setDefaultDimensions(1.,1);
_ia->createBillboard(Ogre::Vector3(0,0,0));
Ogre::SceneNode* iaNode =
_modelo->createChildSceneNode(billNodeAux.str());
iaNode->setPosition(0,6.0,0);
iaNode->attachObject(_ia);
Cuando el estado cambia se realiza adems una invocacin a la funcin setMaterialName(), que cambia la imagen a mostrar.
Lo ms importante de los enemigos es su forma de guiarse por el escenario; esto
se puede explicar respondiendo a una serie de preguntas.
Cmo se mueven los enemigos?
Bueno, para el movimiento de los enemigos se us un pathnder, un algoritmo
A-star para ser ms concreto. Dicho algoritmo devuelve un camino a los enemigos
para que recorran los pasillos del juego sin problemas, dicho camino es devuelto en
puntos y los enemigos van de punto a punto, encarando al punto y trasladndose en su
direccin.
Cmo se evitan las paredes, trampas y columnas ?
En la creacin del escenario se creo un array tridimensional para almacenar cierta
informacin (trampas y bloques). Dicho array se relleno usando unos archivos xml
generados mediante un script de Blender; en dicho array viene la posicion de los
bloques que forman el escenario. Gracias a esto y cogiendo lo que viene a ser el suelo
de array se puede rellenar un mapa del suelo que se le pasa al pathnder. De este modo,
las trampas se guardan en el mapa y se puede calcular nuevas rutas dependiendo de la
inteligencia de los enemigos.
Como evitan chocarse los enemigos?
Los enemigos llevan a sus pies unos bloques pequeos ocultos. Dichos bloques se
usan para calcular colisiones entre cajas AABB.]
Listado B.14: Evasin de colisiones.
1 Vector3 trans(0,0,0);
[1108]
La parte importante son las lneas 15-16 . Dicha funcin comprueba la interseccin
de 2 nodos y devuelve una AxixAlignedBox, si es distinto de Null la caja invisible esta
chocando con otra caja entonces ambas cajas se repelen.
Bibliografa
[1] www.boost.org.
[2] www.cegui.org.uk.
[3] www.swig.org.
[4] ISO/IEC 9241. ISO/IEC 9241-11: Ergonomic requirements for ofce work with
visual display terminals (VDTs) Part 11: Guidance on usability. ISO, 1998.
[5] Advanced Micro Devices, Inc., Disponible en lnea en http://support.
amd.com/us/Processor_TechDocs/31116.pdf. BIOS and Kernel
Developers Guide (BKDG) For AMD Family 10h Processors, Apr. 2010.
[6] T. Akenine-Moller, E. Haines, and N. Hoffman. Real-Time Rendering (3rd
Edition). AK Peters, 2008.
[7] E. Akenine-Mller, T. Haines and N. Hoffman. Real-Time Rendering. AK
Peters, Ltd., 2008.
[8] T. Akenine-Mller, E. Haines, and N. Hoffman. Real-Time Rendering. AK
Peters, 3rd edition, 2008.
[9] Andrei Alexandrescu. Modern C++ Design: Generic Programming and
Design Patterns Applied. Addison-Wesley Professional, 2001.
[10] Grenville Armitage, Mark Claypool, and Philip Branch. Networking and Online
Games. Wiley, 2006.
[11] K. Beck. Extreme Programming Explained: Embrace Change. Addison- Wesley
Professional. Addison-Wesley Professional, 1999.
1109
[1110]
Wordware Game
[17] D. Bulka and D. Mayhew. Efcient C++, Performance Programming Techniques. Addison-Wesley, 1999.
[18] Linux Kernel Community. Git Tutorial. 2013.
[19] James O. Coplien.
February 1995.
C++ Report,
Introduction to
[1111]
[34] Randima Fernando and Mark J. Kilgard. The Cg Tutorial: The Denitive Guide
to Programmable Real-Time Graphics. Addison-Wesley Longman Publishing
Co., Inc., Boston, MA, USA, 2003.
[35] K. Flood. Game unied process (gup).
[36] S. Franklin and Graesser A. Is it an agent, or just a program?: A taxonomy
for autonomous agents. In Proceedings of the Third International Workshop
on Agent Theories, Architectures, and Languages, pages 2135, London, UK,
1997. Springer-Verlag.
[37] E. Gamma. Design Patterns: Elements of Reusable Object-Oriented Software.
Addison-Wesley Professional, 1995.
[38] E. Gamma, R. Helm, R. Johnson, and J. Vlissided. Design Patterns. Addison
Wesley, 2008.
[39] Joseph C. Giarratano and Gary Riley. Expert Systems. PWS Publishing Co.,
Boston, MA, USA, 3rd edition, 1998.
[40] J. L. Gonzlez. Jugabilidad: Caracterizacin de la Experiencia del Jugador en
Videojuegos. PhD thesis, Universidad de Granada, 2010.
[41] T. Granollers. MPIu+a. Una metodologa que integra la ingeniera del
software, la interaccin persona-ordenador y la accesibilidad en el contexto
de equipos de desarrollo multidisciplinares. PhD thesis, Universitat de Lleida,
2004.
[42] J Gregory. Game Engine Architecture. AK Peters, 2009.
[43] Khronos
Group.
COLLADA
http://www.khronos.org/collada/, 2012.
Format
Specication.
source
code.
[1112]
Morgan
Formato
Collada.
El_formato_COLLADA,
[1113]
[75] Gary Riley. CLIPS home site. http://clipsrules.sourceforge.net, 2013.
[76] R.J. Rost and J.M. Kessenich. OpenGL Shading Language. Addison-Wesley
Professional, 2006.
[77] R. Rouse III. Game Design: Theory and Practice. Wordware Publishing, 2001.
[78] W.W. Royce. Managing the development of large software systems.
Proceedings of IEEE WESCON, 1970.
In
[79] S.J. Russell and Norvig. Articial Intelligence: A Modern Approach. Pearson
Education, 1998.
[80] E. Salen, K.; Zimmerman. Rules of Play: Game Design Fundamentals. The
MIT Press, 2003.
[81] H. Schildt. C++ from the Ground Up (3rd Edition). McGraw-Hill Osborne,
2003.
[82] D. Schmidt. Acceptor-connector: an object creational pattern for connecting
and initializing communication services, 1997.
[83] B. Schwab and Inc ebrary. AI game engine programming. Charles River Media,
2004.
[84] P. Shirley and S. Marschner. Fundamentals of Computer Graphics. AK Peters,
3rd edition, 2009.
[85] B. Shneiderman. Universal usability. Communications of the ACM, pages 84
91, 2000.
[86] D. Shreiner. OpenGL Programming Guide: The Ofcial Guide to Learning
OpenGL, Versions 3.0 and 3.1 (7th Edition). Addison-Wesley, 2009.
[87] B. Sierra Araujo. Aprendizaje automtico: conceptos bsicos y avanzados:
aspectos prcticos utilizando el software Weka. Pearson Prentice Hall, Madrid,
2006.
[88] D. Sikora. Incremental game development.
[89] A. Silberschatz, P.B. Galvin, and G. Gagne.
Operativos. McGraw Hill, 2006.
Fundamentos de Sistemas
[90] Jouni Smed and Harri Hakonen. Algorithms and Networking for Computer
Games. Wiley, 2006.
[91] R.M. Stallman and GCC Developer Community. Using GCC: The GNU
Compiler Collection Reference Manual. GNU Press, 2003.
[92] R.M. Stallman, R. Pesch, and S. Shebs. Debugging with GDB: The GNU
Source-Level Debugger. GNU Press, 2002.
[93] B. Stroustrup. The C++ programming language. Addison-Wesley, 2000.
[94] B. Stroustrup. El lenguaje de programacion C++. Pearson Education, 2001.
[95] I. Takeuchi, H.; Nonaka. Scrum: The new product development game, 1986.
[96] K. Tanaka. An introduction to fuzzy logic for practical applications. Springer
Verlag, 1997.
[1114]
[97] S.J. Teller and C.H. Squin. Visibility preprocessing for interactive walkthroughs. In ACM SIGGRAPH Computer Graphics, volume 25, pages 6170.
ACM, 1991.
[98] T. Theoharis, G. Papaioannou, and N. Platis. Graphics and Visualization:
Principles & Algorithms. AK Peters, 2008.
[99] Jos Toms Tocino, Pablo Recio, and Manuel Palomo-Duarte. Gades Siege.
http://code.google.com/p/gsiege, 2012.
[100] T. Ulrich. Rendering massive terrains using chunked level of detail control.
SIGGRAPH Course Notes, 3(5), 2002.
[101] Wang and Niniane. Let there be clouds! Game Developer Magazine, 11:3439,
2004.
[102] G. Weiss. Multiagent Systems: a Modern Approach to Distributed Articial
Intelligence. The MIT Press, 1999.
[103] Wikipedia. Stratego wikipedia, the free encyclopedia, 2014.
accessed 30-May-2014].
[Online;
[104] M. Wooldridge and N.R. Jennings. Intelligent agents: Theory and practice. The
Knowledge Engineering Review, 10(2):115152, 1995.
[105] L.A. Zadeh. Fuzzy sets. Information and control, 8(3):338353, 1965.
[106] H. Zhang, D. Manocha, T. Hudson, and K.E. Hoff III. Visibility culling
using hierarchical occlusion maps. In Proceedings of the 24th annual conference on Computer graphics and interactive techniques, pages 7788. ACM
Press/Addison-Wesley Publishing Co., 1997.