Post on 14-May-2020
transcript
UNIVERSIDAD PONTIFICIA DE COMILLAS
ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA (ICAI) INGENIERO EN INFORMÁTICA
PROYECTO FIN DE CARRERA
COMPONENTE AUTÓNOMO
PARA LA IMPLEMENTACIÓN
DE AGENTES EVOLUTIVOS
EN VIDEOJUEGOS
DANIEL PÉREZ ÁLVAREZ
MADRID, SEPTIEMBRE 2006
Autorizada la entrega del proyecto al alumno:
Daniel Pérez Álvarez
El director del proyecto:
D. Antonio Luis Arranz Matía
Fdo.: ................................ Fecha: ........../........../..........
Vº Bº del coordinador de proyectos:
D. Miguel Ángel Sanz Bobi
Fdo.: ................................ Fecha: ........../........../..........
Agradecimientos
Deseo expresar mi más profundo agradecimiento a todos los que, de un modo u
otro, han hecho posible este trabajo, y muy especialmente:
A D. Miguel Ángel Sanz Bobi y D. José Ángel Olivas Varela, profesores de la
Escuela Técnica Superior de Ingeniería (ICAI), por conseguir transmitirme su
entusiasmo por la inteligencia artificial.
A D. Antonio Luis Arranz Matía, miembro del Instituto de Investigación
Tecnológica (IIT), por proponer un proyecto tan original e interesante, por aceptar
dirigirme en su realización, y por el ánimo y la ayuda prestados en todo momento.
A mis amigos, en particular a Moisés, Emilio, José Luis, Silvia, Fernando y
Sergio, por lograr ponerme de buen humor después de tantas horas delante del
ordenador.
Y por último, a mis padres, por su colaboración en distintos aspectos del proyecto,
y, sobre todo, por la infinita paciencia que tienen conmigo.
iv
Resumen
La inteligencia artificial es usada en los videojuegos modernos con el fin de
simular un comportamiento lo más humano posible en personajes controlados por
el ordenador. El incremento en capacidad de proceso y almacenamiento de
ordenadores y consolas, junto con la progresiva popularidad de géneros como la
estrategia en tiempo real o los juegos de acción en primera persona, están
impulsando importantes mejoras en este campo.
La creciente complejidad de los algoritmos de inteligencia artificial empleados en
estos juegos obliga a invertir cada vez más tiempo y dinero en su desarrollo, por
lo que empiezan a buscarse alternativas como el uso de módulos reutilizables
diseñados por terceros. El objetivo del proyecto es precisamente llenar este vacío
en un mercado cuya demanda no puede sino aumentar, creando un componente
autónomo que permita implementar agentes capaces de adaptarse dinámicamente
a su entorno, en todo tipo de videojuegos.
El componente está basado en el paradigma de la neuroevolución de topologías
aumentativas (NEAT), consistente en hacer evolucionar la estructura y los pesos
de una población de redes neuronales según un algoritmo genético. De este modo
se consiguen individuos progresivamente mejor adaptados y con conductas más
complejas, evitando además la homogeneidad y el carácter predecible de técnicas
más clásicas como las máquinas de estados.
Para probar su funcionamiento se ha construido un prototipo de videojuego que
hace las veces de entorno de experimentación, en el que se ha sometido a los
agentes a distintas pruebas relativas a la navegación y al combate. Una vez
satisfechos con el rendimiento del componente en este entorno, se pasaría a
integrarlo en un videojuego real, concretamente en Half-Life 2.
vi
Los resultados obtenidos en el entorno de experimentación parecen muy
alentadores. Los agentes produjeron estrategias relativamente elaboradas,
encontrando la mejor forma de evitar los obstáculos de los mapas, detectando y
esquivando las fuentes de peligro, e incluso haciendo uso de técnicas propias de
jugadores avanzados, como el backpedaling o el circle strafing.
Se consiguió también crear un módulo de Half-Life 2, a través del cual
controlamos las acciones y la percepción del mundo de los personajes. A la hora
de añadirlos a un mapa nos encontramos con que poblaciones de más de dos
individuos provocaban un error en el juego, por lo que no se pudo realizar un
estudio completo. Sin embargo, la integración parcial que se ha hecho demuestra
vii
que la utilización del componente en un juego real es factible. Una vez
solucionado dicho error, sin duda observaríamos resultados tan positivos como los
obtenidos en nuestro prototipo.
Los algoritmos neuroevolutivos, aunque difíciles de configurar y depurar, son una
potente herramienta a tener en cuenta para la implementación de agentes en
entornos cambiantes y con un gran número de acciones posibles en cada instante,
como ocurre en los videojuegos. El contar con una solución modular que,
realizando unos mínimos ajustes, proporcione las capacidades descritas,
indiscutiblemente supone un avance frente a tener que codificar las rutinas
relativas a la inteligencia artificial de cada proyecto partiendo prácticamente de
cero.
viii
Abstract
Artificial intelligence is used in modern videogames to simulate human-like
behaviors as convincing as possible in non-player characters. The increase in
processor speed and storage capacity of computers and consoles, along with the
great popularity gained by genres like real-time strategy or first person shooters,
are driving important improvements in this field.
The growing complexity of artificial intelligence algorithms employed in these
games forces companies to invest more and more time and money on their
development, raising the interest in alternatives such as re-usable modules
designed by third parties. Our project objective is precisely to fill this void in a
market whose demand can do anything but increase, by creating an autonomous
component that allows adding agents that dynamically adapt to their environment,
in any kind of videogame.
Our component is based on a method called neuroevolution of augmenting
topologies (NEAT), which consists in evolving the structure and weights of a
population of neural networks by means of a genetic algorithm. This way, we
obtain progressively better adapted and more complex individuals, avoiding at the
same time the homogeneity and predictability of classic techniques such as finite
state machines.
In order to test this component, a prototype videogame was built, serving as a test
bed for our experiments. The agents were presented with different scenarios
regarding map navigation and combat. Once satisfied with the results, we would
go on to integrate it into a real videogame, more specifically Half-Life 2.
The results obtained in our test bed experiments seem most encouraging. Agents
managed to produce relatively elaborated strategies, finding the best way to avoid
ix
obstacles on the map, detecting and evading dangers, and even making use of
techniques employed by advanced players, like backpedaling and circle strafing.
We were also able to create a plugin for Half-Life 2 to control the characters
actions and perception of the environment. When adding them to a map, we found
out that populations bigger than two individuals caused an error in the game,
preventing us from carrying out a complete study. Nevertheless, our partial
implementation proves that the use of the component in a real videogame is
feasible. Once the error was solved, we would undoubtedly observe results as
promising as those obtained in our prototype.
x
Neuroevolution algorithms, although difficult to configure and debug, provide a
powerful tool for the implementation of agents in videogames, where
environments are constantly changing, and the number of possible actions
between which to choose at every instant is huge. Having at one’s disposal a
modular solution that, with minimum tweaking, provides the capabilities
described, instead of having to code the artificial intelligence routines for each of
our projects basically from scratch, implies a great advancement.
xi
Índice
1 Introducción .................................................................................................... 1
1.1 Motivación .............................................................................................. 2
1.2 Objetivos ................................................................................................. 5
1.3 Metodología de trabajo ........................................................................... 7
1.4 Recursos a utilizar ................................................................................... 9
2 Descripción de las tecnologías ...................................................................... 10
2.1 Técnicas clásicas ................................................................................... 11
2.1.1 Máquinas de estados finitas (FSMs) ............................................. 11
2.1.2 Máquinas de estados borrosas (FuSMs)........................................ 12
2.1.3 Sistemas expertos .......................................................................... 13
2.1.4 Árboles de decisión....................................................................... 14
2.1.5 Mensajería ..................................................................................... 16
2.1.6 Scripting ........................................................................................ 17
2.2 Redes neuronales (NNs)........................................................................ 19
2.2.1 Redes neuronales en la naturaleza................................................. 19
2.2.2 Redes neuronales artificiales......................................................... 20
2.2.3 Usando una red neuronal............................................................... 23
2.2.4 Cómo funciona una red neuronal .................................................. 24
2.2.5 Ventajas e inconvenientes ............................................................. 26
2.3 Algoritmos genéticos (GAs) ................................................................. 28
2.3.1 Evolución en la naturaleza ............................................................ 28
2.3.2 Evolución artificial........................................................................ 29
2.3.3 Usando un algoritmo genético ...................................................... 30
2.3.4 Ventajas e inconvenientes ............................................................. 31
2.4 Neuroevolución (NE) ............................................................................ 33
2.4.1 Qué aporta la neuroevolución ....................................................... 33
2.4.2 Neuroevolución de topologías aumentativas (NEAT) .................. 34
3 Descripción del modelo desarrollado............................................................ 36
3.1 Implementación del paradigma NEAT ................................................. 37
xiii
3.1.1 El genotipo en NEAT.................................................................... 37
3.1.2 Innovaciones ................................................................................. 40
3.1.3 Operador de cruce ......................................................................... 42
3.1.4 Operadores de mutación................................................................ 44
3.1.5 Gestión de especies ....................................................................... 45
3.1.6 Qué ocurre en una época ............................................................... 46
3.1.7 Del genotipo al fenotipo................................................................ 47
3.2 Desarrollo del entorno de experimentación .......................................... 50
3.2.1 N*E*A*R...................................................................................... 50
3.2.2 Arquitectura del juego................................................................... 51
3.2.3 Mapa.............................................................................................. 52
3.2.4 Entidades de juego ........................................................................ 53
3.2.5 Armas y proyectiles ...................................................................... 54
3.2.6 Torretas ......................................................................................... 56
3.2.7 Agentes.......................................................................................... 58
3.2.8 Visión global del mundo ............................................................... 61
3.3 Integración en Half-Life 2..................................................................... 63
3.3.1 Creación del plugin ....................................................................... 63
3.3.2 Bots en Half-Life 2 ....................................................................... 65
3.3.3 El resultado ................................................................................... 67
4 Análisis de resultados.................................................................................... 69
4.1 En el entorno de experimentación......................................................... 70
4.1.1 Obstáculos (I) ................................................................................ 70
4.1.2 Obstáculos (II)............................................................................... 73
4.1.3 Torretas fijas ................................................................................. 76
4.1.4 Torretas móviles............................................................................ 80
4.1.5 Otros agentes................................................................................. 83
4.2 En Half-Life 2 ....................................................................................... 87
5 Planificación.................................................................................................. 88
6 Valoración económica................................................................................... 92
7 Conclusiones ................................................................................................. 93
8 Bibliografía ................................................................................................... 96
9 Apéndices...................................................................................................... 99
9.1 Glosario ............................................................................................... 100
xiv
9.2 Manual de usuario de N*E*A*R ........................................................ 101
9.3 Formato de los archivos MAP............................................................. 111
9.4 Creación e instalación de un plugin de Half-Life 2 ............................ 120
9.5 Contenido del CD................................................................................ 128
xv
1 Introducción 1
La utilización de la inteligencia artificial (AI) en videojuegos se remonta al
nacimiento de estos alrededor de 1970. Títulos como Pong, Space Invaders o Pac-
Man usaban procedimientos muy simples, combinados con algo de aleatoriedad
para que su comportamiento fuese menos predecible, y hasta hace poco ésta ha
sido la línea seguida en la mayoría de producciones.
Sin embargo, en los últimos años se han observado mejoras importantes en este
campo, principalmente impulsadas por el incremento en capacidad de proceso y
almacenamiento de ordenadores y consolas, y por la aparición de nuevos géneros
que dependen totalmente de una inteligencia artificial convincente para ser
disfrutados.
Este proyecto da un paso más en esa dirección, buscando desarrollar un
componente lo más genérico posible que permita implementar en un videojuego
personajes controlados por la máquina que resulten más dinámicos, más
inteligentes, y, en definitiva, más humanos de cara al jugador.
1
1.1 Motivación
La inteligencia artificial es una de las características que pueden hacer destacar a
un videojuego sobre los demás. En la Tabla 1 se puede ver la evolución de la
inteligencia artificial a lo largo de la historia de los videojuegos [SCHW04].
En sus inicios, los desarrolladores se basaban casi exclusivamente en crear
enemigos con movimientos repetitivos, patrones, o puntos débiles. El juego
consistía en eso precisamente: aprender el comportamiento predeterminado de
cada oponente para poder acabar con él fácilmente y así pasar al siguiente.
Otra técnica muy socorrida era el conceder a los personajes controlados por la
máquina la capacidad de hacer trampas, es decir, darles información sobre el
mundo que el jugador no posee. Esto les llevaba a tomar decisiones que parecían
inteligentes. Por ejemplo, en un juego de estrategia, la máquina sabría el tipo de
unidades y tecnologías que está investigando el jugador en todo momento, y
tomaría las contramedidas necesarias. También es muy típico en los juegos de
carreras, en los que, si la distancia que el jugador saca a los contrincantes es muy
grande, van recortando tiempos de forma inexplicable hasta que le alcanzan.
Aunque hace que el juego entrañe un reto mayor, provoca descontento en el
jugador, ya que suele darse cuenta de que la máquina juega con ventaja.
En los videojuegos modernos, estas técnicas se están abandonando en favor de
rutinas de inteligencia artificial real, mucho más sofisticadas. Lo que en la época
de los videojuegos de 8 bits suponía un 1-2% de la CPU, en la actualidad requiere
por norma un 10-35%, cuando no más [WOOD01].
Dada la creciente complejidad de dichas rutinas, se ha pasado de implementarlas
contrarreloj en los últimos meses de desarrollo a tener equipos designados
exclusivamente para esta tarea. Esto provoca inevitablemente un aumento en unos
costes ya de por sí elevados: producir un juego de GameCube cuesta de media
$822.000, de PlayStation 2 $877.000, y de Xbox $1,85 millones [GAME05a].
2
Para la próxima generación de consolas, dichos costes podrían llegar a duplicarse
[GAME05b].
Tennis For Two – Uno de los primeros videojuegos de la historia.
1958
Sin
IA
Pong – Su enorme popularidad despierta el interés en los videojuegos.
1972
Pursuit, Qwak – Aparecen los primeros enemigos, que se mueven según patrones es ablecidos. t
1974
Gun Fight – Lleva su propio microprocesador, admitiendo más elementos de computación y aleatoriedad.
1975
Space Invaders – Los enemigos se mueven según patrones, pero también disparan. Tiene niveles, puntuación, y dificultad creciente.
1978
Galaxian – Usa la misma fórmula de Space Invaders, pero con patrones mucho más complejos.
1979
Pac-Man – Cada fantasma tiene su propia “personalidad”.
1980
Un ordenador gana a un jugador de ajedrez profesional por primera vez.
1983
Patro
nes
Karate Champ – Juego de lucha que introduce combates 1 vs. 1 contra la máquina.
1984
Herzog Zwei – Pionero en el campo de la estrategia en tiempo real.
1990
Dune II – Establece el formato definitivo de los juegos de estrategia, en el que se basarán sagas de gran éxito como Command & Conquer o Warcraft.
1992
Doom – Uno de los primeros juegos de acción en primera persona.
1993
FSM
s
Quake – Además de suponer un gran avance en el aspecto gráfico, popularizó el juego en red y el uso de bots.
1996
BattleCruiser 3000 A.D. – Primer juego comercial que usa redes neuronales.
1996
Deep Blue vence al campeón del mundo de ajedrez, Gary Kasparov.
1997
Half-Life – Su inteligencia artificial se considera la mejor hasta el momento. En realidad se apoya en gran medida en scripts predefinidos.
1998
The Sims – Usando lógica borrosa y técnicas de vida artificial, consigue una profundidad en la personalidad de los personajes nunca antes vista.
2000
Black & White – Las criaturas utilizan aprendizaje automático inductivo para adaptar sus comportamientos.
2001
F.E.A.R. – Impresionante agresividad y capacidad de coordinación táctica por parte de l s oponentes. o
2005
Técn
icas
sofis
ticad
as
Spore – ¿El siguiente paso en inteligencia artificial en videojuegos?
¿?
Tabla 1 - Evolución de la inteligencia artificial en los videojuegos
3
Para contrarrestar este efecto, cada vez es más común pagar una licencia por
utilizar componentes desarrollados por terceros. Existe una gran variedad de
motores gráficos (Source, Doom 3, Unreal, Torque, Lithtech, Irrlicht, OGRE) y de
simulación física (Havok, Ageia, Vortex, Karma, Tokamak, ODE), que gozan de
gran propularidad. En cuanto a inteligencia artificial, empiezan a verse algunos
proyectos en esa línea (PathEngine, Kynapse, Soar, AI.implant, DirectIA,
RenderWare AI, SimBionic, Northstar, SpirOps), pero por el momento no parecen
tener tanta aceptación [GMAI06, GSTR03a, GSTR03b, GSTR03c, GSTR03d,
GSTR03e].
El disponer de un motor de inteligencia artificial que pudiese implementar, en
videojuegos de cualquier género, agentes que se adaptasen dinámicamente al
jugador significaría una revolución. Destaca, pues, el carácter estratégico del
proyecto, y la oportunidad de llenar un vacío en un mercado cuya demanda sólo
puede crecer.
4
1.2 Objetivos
El objetivo final del proyecto es el desarrollo de un componente autónomo y
comercializable que permita la implementación de agentes que evolucionen según
los estímulos del mundo que les rodea, en todo tipo de videojuegos.
Durante el desarrollo de dicho componente se creará un prototipo de videojuego
en 2D a modo de entorno de experimentación, que nos ayude a probar los
distintos algoritmos y configuraciones de una forma más cómoda y controlada.
Cuando se tenga un producto medianamente maduro y estable, se intentará
integrar dentro del videojuego de última generación Half-Life 2 [HL2_06], basado
en el motor Source desarrollado por Valve Software [VALV06]. El motivo, aparte
de su gran popularidad y espectacularidad, es la relativa facilidad con que se
pueden introducir modificaciones mediante el uso de módulos o plugins.
Por tanto, los problemas a abordar son los siguientes:
1. Investigación acerca de la neuroevolución
a. Redes neuronales
b. Algoritmos genéticos
c. Redes neuronales de topología evolutiva
2. Desarrollo del componente autónomo que, haciendo uso de dichas
técnicas, permita implementar agentes que se adapten dinámicamente
a su entorno
a. Modelado de la red neuronal
b. Codificación de la estructura de la red neuronal en forma de genes
c. Creación del algoritmo genético que haga evolucionar la topología
de la red neuronal
5
3. Creación del prototipo de videojuego para probar el componente
a. Interfaz gráfica
b. Movimiento de los agentes en el mapa
c. Combate entre agentes
4. Familiarización con el motor Source
a. Creación de un plugin de Half-Life 2
b. Control de las acciones
c. Percepción del mundo
5. Integración del componente en Half-Life 2
6
1.3 Metodología de trabajo
Lo primero será documentarse sobre las técnicas de inteligencia artificial de las
que se va a hacer uso, especialmente redes neuronales y algoritmos genéticos.
Cuando se tenga un conocimiento suficiente de ellas, se pasará a estudiar el
campo de la neuroevolución.
Hecho esto, se desarrollará un modelo software de una red neuronal, para
posteriormente codificar su estructura en forma de genes. Esto posibilitará
aplicarle un algoritmo genético que haga evolucionar su topología.
En paralelo con este modelo, se construirá un prototipo de videojuego de acción
en 2D para probarlo. En él se introducirán agentes, cada uno de los cuales tendrá
como cerebro una red neuronal de topología evolutiva. Diversos sensores en el
agente servirán de entrada a la red, que procesará la información y dará una salida
que determinará las acciones a realizar (moverse, atacar, etc.).
En este entorno de experimentación se estudiará el comportamiento de los agentes
en distintas situaciones (Figura 1):
• Movimiento de los agentes en un mapa con obstáculos.
• Combate con torretas fijas.
• Combate con torretas móviles.
• Combate con otros agentes.
7
Obstáculos
Torretas móviles
Torretas fijas
Otros agentes
Figura 1 - Comportamiento de los agentes en distintas situaciones
Una vez que este prototipo funcione satisfactoriamente, se avanzará a la fase de
integración del componente en Half-Life 2. Para ello habrá que familiarizarse con
el equipo de desarrollo de software (SDK) que proporciona la compañía creadora
del juego, Valve. Lo primero será crear un plugin con la funcionalidad mínima,
para luego estudiar cómo controlar las acciones de los personajes y percibir el
mundo a través de sus ojos.
Conseguido esto, se podrá integrar el componente de inteligencia artificial
desarrollado dentro de Half-Life 2.
8
1.4 Recursos a utilizar
Para el desarrollo del modelo de red neuronal de topología evolutiva se usará
como guía el trabajo de Kenneth Owen Stanley y Risto Miikkulainen en la
Universidad de Texas [NNRG06].
El prototipo de videojuego 2D será desarrollado desde cero, contando con la
ayuda de los artículos y libros que sobre este tema consigamos encontrar.
Para el plugin de Half-Life 2 nos basaremos en las interfaces diseñadas por Valve
para su motor Source, y en código de ejemplo incluido en el SDK que facilitan.
Dado que el SDK está escrito por entero en C++, y que está preparado para ser
compilado en Visual Studio .NET 2003, se usarán ese lenguaje y entorno de
programación a lo largo de todo el proyecto para evitar tener que reescribir el
código por algún problema de compatibilidad que surgiese en el último momento.
Para el control de versiones, tanto de código fuente y ejecutables como de
documentación, se usará Subversion.
9
2 Descripción de las
tecnologías 2
A continuación se exponen algunas de las técnicas utilizadas en la inteligencia
artificial de los videojuegos actuales.
Se empezará con un breve resumen de las más populares, como las máquinas de
estados, los árboles de decisión, o el scripting, para luego profundizar en las que
realmente se van a usar en este proyecto, que son las redes neuronales y los
algoritmos genéticos.
Finalmente se explicará el concepto de neuroevolución, y el paradigma NEAT.
10
2.1 Técnicas clásicas
Las siguientes técnicas de inteligencia artificial se consideran hoy por hoy las más
relevantes en el mundo de los videojuegos.
2.1.1 Máquinas de estados finitas (FSMs)
Una máquina de estados finita es un sistema con un número finito de estados en
los que puede estar, y que puede operar con las entradas bien para hacer
transiciones de un estado a otro, bien para provocar una salida. Una FSM sólo
puede estar en un estado en un momento dado.
En la Figura 2 puede verse una FSM que modelaría el comportamiento de un
personaje muy simple con tres estados: no hacer nada, atacar y huir.
Figura 2 - FSM de un personaje muy simple
En la historia de los videojuegos, pocas estructuras de datos han sido usadas tanto
como la FSM. A pesar de su sencillez, resulta sumamente útil para dividir un
problema en partes más pequeñas, y por ende más manejables.
La principal ventaja de las FSMs es su simplicidad. Son fácilmente visualizables,
y, teniendo un buen diagrama de estados, el código prácticamente se escribe solo.
Además, dada su flexibilidad, pueden ser usados desde en el movimiento de los
Esperar
Enemigo a la vista
No hay enemigos
No hay enemigos
Enemigo demasiado fuerteAtacar Huir
11
personajes hasta en los diálogos. Finalmente, su depuración es trivial: todo es
determinista, se pueden replicar directamente los errores encontrados, y la lógica
del problema está centralizada.
Esta ventaja, su simplicidad, muchas veces se convierte también en su mayor
inconveniente. Se suelen programar informalmente, sin hacer un diseño previo, lo
que lleva a un sistema fragmentado y problemático a la hora de su mantenimiento.
A esto hay que añadir su tendencia a crecer a lo largo del proyecto, según se
intentan incluir comportamientos cada vez más especializados para los personajes.
Este hecho provoca que los diagramas se hagan sumamente complejos, al crecer
exponencialmente el número de transiciones respecto al de estados, llegando a un
punto en el que se vuelven inmanejables.
2.1.2 Máquinas de estados borrosas (FuSMs)
Las máquinas de estados borrosas se basan en el concepto de lógica borrosa,
superconjunto de la lógica booleana convencional que permite manejar verdades
parciales. Como las FSMs, las FuSMs llevan el registro de los posibles estados de
juego. Pero, a diferencia de las FSMs, que tienen un único estado actual y
responden a las entradas pasando a otro estado diferente, las FuSMs pueden estar
en diversos estados al mismo tiempo, con lo que no hay transiciones. Cada estado
tiene un nivel de activación, que determina hasta qué punto el sistema se
encuentra en él. El comportamiento global del sistema viene por tanto
determinado por la combinación de las contribuciones de los estados activos en
ese momento.
Las FuSMs sólo son útiles para sistemas que puedan estar en más de un estado a
la vez, y para los que valores booleanos como encendido o apagado, abierto o
cerrado, vivo o muerto, resulten insuficientes. Valores borrosos serían medio
encendido, casi cerrado, o no muerto del todo. Normalmente se usa un coeficiente
entre 0.0 y 1.0 para representar el grado de pertenencia a un estado (0.0
significaría totalmente apagado, y 1.0 totalmente encendido, por ejemplo).
12
El diseño e implementación de una FuSM es sencillo, dado un problema
adecuado. Si la solución requiere estados autónomos y concurrentes, este modelo
posibilita diseñarlos como tal. Nos evita tener que trabajar con los eventos y
transiciones a los que nos teníamos que enfrentar en las FSMs. Cada estado se
activa según la escala que se defina para este problema particular, y es
independiente de los demás.
Las FuSMs son mucho más escalables que las FSMs, de nuevo por la naturaleza
desacoplada de los estados. Un problema que puede surgir es la excesiva mezcla
de comportamientos. Cuantos más estados afecten a una característica concreta
del sistema, más diluida queda la aportación de cada uno de ellos. Esto puede dar
lugar a resultados inesperados.
Su depuración también es bastante simple. Al no depender un estado de los
demás, se pueden desactivar aquellos que no nos interesen para concentrarnos en
el resto.
El inconveniente de las FuSMs es que su aplicación es mucho más limitada que la
de las FSMs. Mientras que estas últimas modelan comportamientos que suceden
secuencialmente, las primeras son más adecuadas para sistemas en los que se
puedan obtener comportamientos complejos mediante la mezcla de
comportamientos independientes más sencillos. Esto no siempre es lo que se
necesita, o se desea, en un videojuego.
2.1.3 Sistemas expertos
Los sistemas expertos intentan capturar y explotar los conocimientos de un
experto humano en un área muy específica. El sistema experto almacena esta
información en forma de reglas de producción en la base de conocimientos. En la
base de hechos se encuentra una descripción del estado actual del entorno de la
aplicación. Combinando la información de ambas, realiza razonamientos en
respuesta a cuestiones que se le planteen, proporcionando un resultado similar al
13
que el experto humano habría dado. En la Figura 3 se puede ver un esquema de
un sistema experto.
Base de hechos Base de
conocimientos
Agenda
Motor de inferencia
Conclusión
Figura 3 - Componentes de un sistema experto
Se pueden usar sistemas expertos en videojuegos, por ejemplo para la
planificación de tareas, como memoria, o como dispositivo de aprendizaje, pero
por el momento no tienen demasiada popularidad. Computacionalmente pueden
ser muy costosos, sobre todo en juegos con una gran cantidad de reglas de
producción. Aún así, hay proyectos que demuestran la factibilidad de la idea. Soar
[SOAR06], un proyecto iniciado por Alan Newell, John Laird y Paul Rosenbloom
en la Universidad de Carnegie Mellon, ha conseguido integrar sistemas expertos
en juegos como Descent 3 o Quake 2. Usando un sistema con más de 700 reglas,
consiguieron un agente de Quake 2 capaz de navegar por mapas arbitrarios,
recoger objetos, usar armas, y plantar cara a jugadores humanos haciendo uso de
estrategias relativamente complejas.
2.1.4 Árboles de decisión
Los árboles de decisión son usados para generalizar a partir de casos particulares.
Permiten identificar conceptos, o clases de objetos, a partir de las características
14
de un conjunto de ejemplos que los representan. La información extraída de los
mismos queda organizada jerárquicamente en forma de árbol.
El árbol de decisión se construye a base de ir haciendo preguntas acerca de
distintos aspectos de los ejemplos, y clasificándolos según la respuesta. Estas
posibles respuestas son excluyentes entre sí, lo que hace que, a partir de casos
desconocidos y siguiendo el árbol adecuadamente, se llegue a una única
conclusión o decisión a tomar. Podríamos usarlo por ejemplo para averiguar el
tipo de comportamiento de un humano en un juego de acción en primera persona
(FPS), y que nuestros agentes actúen en consecuencia (Figura 4).
¿Se mueve?
No Sí
Figura 4 - Árbol de decisión para clasificar el comportamiento de un jugador en un FPS
Los árboles de decisión son fáciles de usar y entender. Nos pueden servir para
hacer clasificaciones, de un modo más o menos burdo, que nos muestren
conexiones entre las características de los ejemplos que de otro modo podríamos
no haber descubierto. El resultado viene dado además en forma de reglas lógicas,
lo que significa que, en caso de que hiciese falta, podríamos ajustarlas a mano sin
mayor complicación.
El inconveniente que tienen los árboles de decisión, como otros algoritmos de
inducción, es el sobreajuste: aprender demasiado sin tener capacidad de
generalización. Esto se puede evitar añadiendo un mecanismo de poda, es decir,
deteniendo el crecimiento del árbol antes de que se ajuste perfectamente a los
Camper ¿Ataca? No Sí
Defensivo Ofensivo
15
datos. Perdemos algo de precisión en la clasificación, pero la hacemos más rápida
y con mayor ratio de aciertos en la predicción de casos no vistos ante la presencia
de ruido.
Algoritmos como ID3 (sin poda) y C4.5 (con poda) construyen automáticamente
árboles de decisión a partir de un conjunto de ejemplos de entrenamiento.
2.1.5 Mensajería
En el mundo de la programación de videojuegos, si hay una técnica más usada
que las máquinas de estados es la mensajería. El concepto de mensaje, o evento
como también se suele llamar, es simple: en vez de que una entidad A tenga que
comprobar si se ha producido algún cambio en la entidad B, constantemente o
cada cierto tiempo, A es informada de dicho cambio mediante un mensaje enviado
de B a A en el momento en que ocurra. De este modo no tenemos que
desperdiciar ciclos del procesador comprobando constantemente si ha ocurrido
algo, sino que es el juego el que informa a las entidades de los eventos en los que
están interesadas, despreocupándose hasta que llegue otro evento.
A diferencia de las técnicas vistas hasta ahora, la mensajería no es una estructura
para la toma de decisiones, sino para la organización y optimización de las
comunicaciones entre los objetos del juego.
La mensajería tiene pocas desventajas. Su sencillez y facilidad de comprensión
hacen que sea cómoda de manejar, y su naturaleza distribuida basada en
notificaciones, frente a la computación centralizada basada en sondeos continuos,
supone una carga mucho menor para la CPU, aunque requiera algo más de
memoria para llevar registro de los mensajes a transmitir.
Usando un sistema de mensajería como forma de comunicación, podemos escribir
las distintas partes del juego de forma totalmente modular. Los cambios que
realicemos internamente en una clase no afectarán al resto, ya que la interfaz con
el exterior es a través de eventos. Para su depuración, además, sólo tenemos que
16
fijarnos en el flujo de mensajes enviados y recibidos. Podríamos incluso grabar
dicho flujo para posteriormente reproducirlo y replicar comportamientos o errores
encontrados.
El único problema de la mensajería es que se pierde algo de control con respecto a
un sistema basado en estados. No obstante, esto puede ser fácilmente subsanado
añadiendo prioridades a los eventos.
2.1.6 Scripting
Las técnicas vistas hasta ahora requieren programadores dedicados a
implementarlas, depurarlas y extenderlas. Sin embargo, puede que estos
programadores no sean los encargados del contenido creativo del juego, o que la
cantidad de contenido sea demasiado grande para el número de programadores
disponible.
Una forma muy común de conseguir que más personas puedan colaborar en la
inteligencia artificial del juego, sin tener que contratar más programadores, es
mediante scripting. Scripting significa usar un lenguaje de programación
simplificado para eliminar las barreras de entrada que implican el aprender un
lenguaje de programación de verdad. Esto habilita a gente con menos
conocimientos técnicos a construir elementos de inteligencia artificial,
comportamientos, o lógica del juego. El árbol de conversaciones de una aventura
gráfica, los detalles de cada movimiento en un juego de lucha, o la forma de
coordinar ataques de un grupo de enemigos, son susceptibles de ser programados
usando scripting. Los lenguajes de scripting pueden ir desde lo muy simple (sólo
permitiendo asignación de valores a variables, por ejemplo) hasta completos
lenguajes de programación (Neverwinter Nights directamente usa Java).
Mediante scripting también podemos hacer uso de técnicas de prototipado rápido.
Toda la lógica del juego puede programarse en un lenguaje de scripting, para
luego optimizar las partes críticas portándolas a C o C++. Al mismo tiempo se
17
elimina la necesidad de recompilar el programa cada vez que se hace un cambio
en el código, acelerando sobremanera su desarrollo.
El scripting también tiene aspectos negativos. La velocidad de un lenguaje
interpretado siempre va a ser menor que la de uno compilado. Su depuración
también se hace más difícil, al no disponer de herramientas tan avanzadas como
en los lenguajes grandes. Asimismo influye el hecho de que las personas que
modifican los scripts muchas veces no tienen los conocimientos de un
programador, cometiendo un mayor número de errores.
El scripting es una herramienta muy potente, pero conviene usarla con prudencia,
ya que implica tener que mantener un sistema adicional aparte del juego en sí.
18
2.2 Redes neuronales (NNs)
Las redes neuronales, también llamadas redes neuronales artificiales (ANNs) para
distinguirlas de las biológicas, son algoritmos matemáticos con capacidad para
recordar experiencias y hacerlas disponibles para su uso.
Están basadas hasta cierto punto en el funcionamiento de un cerebro, a nivel tanto
organizativo como funcional. Aunque no son un modelo muy realista, sí que
resultan útiles para el reconocimiento de patrones, y para predecir tendencias en
conjuntos de datos.
2.2.1 Redes neuronales en la naturaleza
Los cerebros de los animales consisten esencialmente en un gran número de
células nerviosas, llamadas neuronas, conectadas entre sí. Cada neurona tiene una
serie de conexiones, tanto de entrada como de salida, con otras neuronas. Las
conexiones de entrada a la neurona se llaman dendritas, y las de salida se
denominan axones. Las dendritas de una neurona se encuentran muy próximas a
los axones de otras, quedando entre ellas un espacio de unas 0.01 micras
denominado espacio sináptico o sinapsis. En la Figura 5 se puede ver un diagrama
con las partes de la neurona [BUCK02].
Figura 5 - Partes de una neurona
19
Una descripción muy simplificada de lo que ocurre en la neurona es que las
dendritas reciben impulsos eléctricos de los axones próximos a ellas. La neurona
se va cargando hasta que, al llegar a un cierto umbral, descarga la energía
acumulada a través de su axón, transmitiéndola a las dendritas de otras neuronas.
Si una neurona se activa con suficiente frecuencia, se producirán pequeños
cambios de carácter biológico en ella (como una disminución en la resistencia
eléctrica a lo largo de las dendritas y el axón, una mayor sensibilidad en los
terminales sinápticos, o incluso modificaciones en el tamaño de las fibras
nerviosas), provocando que la cantidad de electricidad necesaria para que la
neurona se active disminuya. La neurona ha “aprendido” que tiene que activarse
frecuentemente, y lo hará requiriendo menos intensidad de corriente. También
puede ocurrir lo contrario: si una neurona no se activa casi nunca, tiende a
atrofiarse.
En definitiva, los cerebros de los animales funcionan tomando unas entradas,
reconociendo patrones en ellas, y tomando decisiones basadas en dichos patrones.
El cerebro humano tiene unos 100.000 millones de neuronas, cada una con
alrededor de 10.000 conexiones, y opera a unos 100Hz, una fracción de la
velocidad de los procesadores actuales. Sin embargo, mientras que un procesador
sólo puede manejar unas pocas instrucciones a la vez, el cerebro humano lleva a
cabo millones de operaciones en paralelo [BUCK02, SCHW04].
2.2.2 Redes neuronales artificiales
El objetivo de las redes neuronales artificiales es emular esta capacidad de
paralelización del cerebro humano. Están construidas a partir de neuronas
artificiales, un modelo simplificado de una neurona real. La Figura 6 muestra una
neurona artificial.
20
w1
Figura 6 - Neurona artificial
Cada entrada a la neurona artificial lleva un peso asociado, y son estos pesos los
que determinan la actividad de la red neuronal. Las entradas a la neurona son
multiplicadas por sus respectivos pesos y después sumadas. La función de
activación toma este resultado y calcula la salida a partir de él. Algunas de las
funciones de activación más comunes se pueden ver en la Figura 7.
Igual que las neuronas del cerebro están interconectadas, las neuronas artificiales
también se conectan entre sí para formar una red neuronal. Hay muchas formas de
conectar estas neuronas, pero probablemente la más sencilla de entender, y la que
más nos interesa para este proyecto, es la red feedforward. Este tipo de red
neuronal artificial toma su nombre de la forma en que cada capa de neuronas
“alimenta” su salida a la siguiente capa, hasta llegar a obtener la salida. En la
Figura 8 se tiene un diagrama de una red feedforward.
∑ xiwi w2
wn
x1
x2
xn
Umbral
φ(u) y
Entradas Pesos sinápticos
Sumador
Función de activación
Salida
u
θ
21
φ(u)
1
u
Umbral
φ(u)
1
0,5
-0,5 0 0,5
Rampa
φ(u)
1
0
0,5
Sigmoidal con distintas pendientes
Figura 7 - Distintas funciones de activación
22
Capa de entrada Capa oculta Capa de salida
Figura 8 - Diagrama de una red feedforward
2.2.3 Usando una red neuronal
El primer paso para implementar una red neuronal es elegir una estructura
adecuada a la naturaleza del problema a resolver. La estructura se refiere tanto al
tipo de red (feedforward, recurrente, lattice, etc.) como a su organización. No
existe un método para determinar el número de neuronas o de capas ocultas en
una red feedforward. No obstante, ya que la velocidad de la red disminuye al
aumentar su complejidad, convendrá hacerla tan pequeña como sea posible.
El paso siguiente al diseño de la red es su entrenamiento. Mediante este proceso,
los pesos de las conexiones sinápticas de las neuronas son adaptados a través de
una continua estimulación del entorno. Existen tres paradigmas de aprendizaje en
redes neuronales: supervisado, no supervisado, y por reforzamiento.
El aprendizaje supervisado implica tener una serie de datos de entrada junto con
sus respectivas salidas esperadas. Se alimenta la red con la entrada, y se ajustan
los pesos de las conexiones si existe discrepancia entre la salida real y la esperada.
El entrenamiento continúa hasta que se alcanza un determinado nivel de precisión.
El aprendizaje no supervisado consiste en analizar estadísticamente la salida y
ajustar los pesos como corresponda. En el aprendizaje por reforzamiento tampoco
23
se conoce la salida esperada, pero se refuerza el comportamiento de la red cuando
actúa correctamente.
Por último, lo único que queda es validar la red neuronal usando ejemplos reales.
2.2.4 Cómo funciona una red neuronal
Donde las redes neuronales resultan verdaderamente útiles es en tareas de
regresión y clasificación [SCHW04]. En la Figura 9 se puede ver un ejemplo de
regresión, y en la Figura 10 uno de clasificación.
Figura 9 - Ejemplo de regresión
Entrenamiento
Prueba
y
x
y
x Ajuste excesivo Ajuste insuficiente
y
x Ajuste adecuado
24
Figura 10 - Ejemplo de clasificación
La regresión consiste en encontrar una función que se ajuste a todos los ejemplos
proporcionados, con una cierta tolerancia. Por ejemplo, supongamos que
queremos crear una red neuronal para que los personajes controlados por la
máquina sean capaces de esquivar los disparos del jugador moviéndose
perpendicularmente a su trayectoria. Las entradas a la red serían la posición del
personaje, la dirección en que está mirando, y la posición del contrincante
humano. Algorítmicamente, lo que necesitaríamos sería calcular el vector que une
ambas posiciones, hacer el producto escalar de éste con la dirección en que está
mirando el personaje para obtener el ángulo entre los vectores, y girar lo
suficiente para esquivar el fuego enemigo. La Figura 11 ilustra esta idea.
x2
x1
Figura 11 - Esquivando los disparos del enemigo
Si combinásemos estas operaciones en una gran función matemática,
conseguiríamos exactamente la misma función que la red neuronal intenta
aprender para resolver el problema. Si se tratase de una función lineal, la red
neuronal encontraría la solución rápidamente, sin tener que usar neuronas ocultas.
En este caso, tratándose de función compleja, hará falta por lo menos una capa
oculta para poder realizar la regresión.
25
En la otra tarea en la que destaca una red neuronal, la clasificación, lo que le
estamos pidiendo es que categorice los datos en varios grupos. El número de
entradas determina las dimensiones del espacio de búsqueda (con dos entradas, la
red trabajaría en un espacio bidimensional), y las salidas representan líneas (o
planos, o hiperplanos) de separación dentro del mismo.
Esta forma de ver la red neuronal, en la que cada entrada equivale a un eje en un
gráfico, y cada salida a una línea que divide los ejemplos en categorías aisladas
unas de otras, permite explicar ciertos aspectos de las redes neuronales. Incorporar
nodos de entrada innecesarios complica el problema, incrementando las
dimensiones del espacio de búsqueda, y cuantos más nodos de salida tengamos,
más precisas serán las categorías, pero también hará que su diferenciación sea más
dificultosa. Aunque es una visión muy simplificada, ayuda a entender el
funcionamiento de estas redes.
2.2.5 Ventajas e inconvenientes
A continuación se presentan las principales ventajas de las redes neuronales:
• Son una buena forma de encontrar relaciones abstractas entre los datos de
entrada.
• Almacenan todo tipo de información, de una forma muy optimizada.
• Pueden obtener soluciones a funciones matemáticas muy complejas. Estas
funciones son aproximadas mediante la modificación de los pesos de la
red, por lo que cuando haya que usarla con datos reales nos ahorraremos el
coste de CPU de los cálculos matemáticos.
• Tienen gran capacidad para extraer significado de conjuntos de datos no
lineales o imprecisos. Pueden generalizar conexiones en absoluto
intuitivas para un experto humano.
• Su entrenamiento sólo constituye una fracción del tiempo que pueden
llevar los métodos de prueba y error, una vez se ha encontrado un diseño
de la red apropiado.
26
No están exentas de inconvenientes, no obstante. He aquí algunos:
• Simplemente desplazan el problema. Si antes era encontrar la forma de
resolverlo, ahora es cómo diseñar la red y entrenarla adecuadamente para
que lo resuelva.
• No son mágicas. Si se le proporcionan datos arbitrarios o ambiguos, la red
encontrará algún tipo de relación entre ellos, pero probablemente no dará
la salida buscada. De hecho, las redes neuronales son famosas por
aprender lo que no queremos que aprendan. La dificultad de entrenar una
red estriba en filtrar los datos de entrenamiento de aquellas relaciones
indeseadas. Eso sí, uno no se da cuenta de que la red ha aprendido algo
erróneo hasta que ha sido entrenada, y muchas veces ni aún así.
• Una red neuronal es una caja negra, y por tanto imposible de depurar. Una
vez entrenada, los pesos obtenidos son en general incomprensibles. Si hay
algo que no cuadra, lo normal es reajustar los datos de entrenamiento y
volver a empezar.
• Son difíciles de implementar, dado el gran número de parámetros a
determinar sin ayuda de reglas o guías: el número de capas ocultas, el
número de nodos por capa, la función de activación, la tasa de aprendizaje,
etc.
• No escalan demasiado bien. Redes de más de mil nodos no son comunes,
ni demasiado estables. La razón de esto puede ser la explosión de
dimensiones del espacio de búsqueda, que provoca una excesiva libertad
de movimiento. Afortunadamente, en un juego no hay necesidad de usar
redes tan grandes.
27
2.3 Algoritmos genéticos (GAs)
En ocasiones nos encontramos con problemas difíciles de resolver, bien por su
excesivo coste computacional, bien porque no se dispone del tiempo suficiente.
Por ejemplo, ajustar los parámetros de la simulación física usada por cada uno de
los 500 coches de Gran Turismo 4 sería uno de estos problemas. Completar una
tarea así dentro de un presupuesto y tiempo razonables parece imposible.
Los algoritmos genéticos aplican las lecciones aprendidas de la ciencia de la
evolución para tratar de encontrar soluciones novedosas a este tipo de problemas.
2.3.1 Evolución en la naturaleza
El proceso de evolución en la naturaleza funciona del siguiente modo. Para
sobrevivir, toda criatura debe ser capaz de reproducirse. La reproducción consiste
en ejecutar las reglas necesarias para generar un nuevo organismo. Estas reglas
son almacenadas en cadenas de DNA llamadas cromosomas, que se encuentran en
toda célula que forma parte del individuo. En la Figura 12 se ve la estructura en
doble hélice del DNA [BUCK02].
Figura 12 - Estructura en doble hélice del DNA
28
Los cromosomas están a su vez compuestos de secuencias de bases (timina,
adenina, citosina y guanina, más concretamente) llamadas genes. Cada gen
codifica un cierto rasgo del organismo, como el color del pelo o la forma de las
orejas. Las diferentes expresiones a las que un gen puede dar lugar, como color de
pelo negro, castaño o rubio, se denominan alelos, y su posición a lo largo del
cromosoma, locus.
Cuando dos progenitores se reproducen, su DNA se divide, obteniendo el hijo la
mitad del DNA de cada uno. A este proceso se le conoce como cruce o
recombinación genética. Si la nueva mezcla de rasgos genéticos resulta buena,
permitirá al hijo vivir y reproducirse. De no ser así, puede que no sobreviva lo
suficiente, o que directamente no sea capaz de reproducirse. Después de muchas
generaciones, dado que los individuos con mejores rasgos tienen más
probabilidades de reproducirse, la población evolucionará hacia una versión
genéticamente superior de su especie.
Cuando los padres pasan el material genético a su descendencia, existe una
pequeña probabilidad de que se vea ligeramente modificado. A este fenómeno se
le llama mutación. En la mayor parte de los casos esto resulta en rasgos negativos,
pero a veces provoca que el hijo de algún modo incremente sus posibilidades de
reproducción.
Por tanto, esta “supervivencia del más apto” hace que el conjunto de rasgos de
una especie, llamado genoma, vaya cambiando gradualmente hacia un modelo
ideal que contendría los genes mejor adaptados para esa criatura en el entorno
actual [BUCK02, SCHW04].
2.3.2 Evolución artificial
Este algoritmo evolutivo puede ser implementado en nuestros programas para
ajustar comportamientos y parámetros que, de hacerlo a mano, llevaría demasiado
tiempo. La gran mayoría de los juegos que usan este tipo de algoritmos, una vez
que llegan al usuario, tienen desactivados todos los comportamientos evolutivos.
29
Aún así, existen notables excepciones como Creatures o Black & White, cuya
esencia es precisamente este tipo de comportamientos.
Los algoritmos genéticos pertenecen a los llamados métodos de búsqueda
estocásticos, que significa que se basan en un elemento de aleatoriedad para
dirigir la búsqueda. Por ello, requieren numerosas iteraciones para conseguir una
solución que se acerque al óptimo global deseado, pero es menos probable que se
queden estancados en un óptimo local. El precio a pagar por esta aleatoriedad es
que no se garantiza ni el rendimiento ni el éxito de la búsqueda. De hecho, los
algoritmos genéticos tienden a ser computacionalmente costosos, por lo que se
emplean mayoritariamente en trabajos offline. El aumento en la capacidad de
proceso de los ordenadores actuales ha posibilitado que empiecen a ver un uso
más generalizado.
2.3.3 Usando un algoritmo genético
Lo primero que hay que hacer cuando se trabaja con un algoritmo genético es
generar una población inicial de individuos, de forma totalmente aleatoria para no
producir sesgos. El tamaño de la población inicial es arbitrario hasta cierto punto:
una población pequeña tiene el riesgo de no cubrir adecuadamente el espacio de
búsqueda, pero una demasiado grande puede ser costosa de tratar en tiempo y
recursos. Conviene experimentar para encontrar el valor que mejor se adecue a
nuestro problema.
Cada individuo es entonces evaluado según una función objetivo o fitness, que
devuelve un valor numérico representando la aptitud del individuo. La elección de
esta función es crucial para el buen funcionamiento del algoritmo genético.
Una vez que se ha calculado el fitness de todos los miembros de la población, se
selecciona a unos cuantos para reproducirse. Si seleccionamos sólo a los mejores
puede darse un problema de convergencia prematura hacia un óptimo local, al
haber excluido demasiados genes, pero si seleccionamos demasiado
aleatoriamente puede darse el fenómeno contrario, una convergencia muy lenta.
30
Una vez escogidos los padres, aplicamos métodos que simulan el cruce y la
mutación, y obtenemos la siguiente población, que con un poco de suerte estará
más cerca de la solución.
2.3.4 Ventajas e inconvenientes
Aunque los algoritmos genéticos no son muy usados en videojuegos, en
determinadas áreas funcionan particularmente bien, entre ellas:
• Cuando se tiene una serie de parámetros que interaccionan de forma
complicada.
• Cuando se tienen muchos mínimos locales y se busca el global, como era
el caso de los coches de Gran Turismo 4.
• Para soluciones que implican una salida discontinua.
• Como complemento a otras técnicas de inteligencia artificial.
• Cuando el cálculo de la solución es demasiado costoso. Si se puede
encontrar un algoritmo genético que encuentre una solución adecuada,
ahorraremos bastante tiempo de CPU. Además, como ocurría en las redes
neuronales, una vez tenemos la solución podemos implementarla sin
preocuparnos de dónde ha salido.
• Cuando no se sabe como resolver el problema. Con un algoritmo genético
empezaremos a obtener resultados inmediatamente.
Pero también tienen sus defectos, entre los que cabe destacar:
• El proceso de evolución toma tiempo. Normalmente se necesitan muchas
generaciones para obtener resultados convincentes. A esto hay que añadir
que puede que tengamos que modificar los parámetros del algoritmo
genético (probabilidades de cruce y mutación, función objetivo, etc.) con
relativa frecuencia, obligándonos a reiniciar el proceso.
• No garantiza una solución óptima, como sucede con todo método
estocástico.
31
• El rendimiento es muy variable. Con la infinidad de formas de codificar
genéticamente un problema, la variedad de operadores de selección, cruce
y mutación, la cantidad de parámetros a configurar, y la libertad para
elegir la función objetivo, encontrar un algoritmo genético que funcione
bien con nuestro problema puede llevar muchos intentos. El único modo
de saber cuándo conviene cada cosa es experimentar.
• Los errores pasan desapercibidos. Cuando un algoritmo genético no
funciona, no sabes si es porque la probabilidad de mutación elegida es
excesiva y no converge a una solución, o si está tendiendo a un óptimo
local y necesita más mutaciones. Incluso puede que haya un error en la
función objetivo o en el operador de cruce. Hay que tener mucho cuidado
al programar un algoritmo genético.
• Es difícil añadir funcionalidad. Si no lo hemos previsto, hasta el más
mínimo cambio podría significar tener que volver a empezar de cero en
cuanto a codificación del problema. Ésta probablemente sea la causa de
que los algoritmos genéticos no hayan tenido mucha aceptación en el
mundo de los videojuegos, en los que hasta el último momento se están
retocando cosas.
32
2.4 Neuroevolución (NE)
En áreas de trabajo donde no tenemos objetivos concretos que nos indiquen la
acción correcta para cada situación, como la robótica o los videojuegos, los
comportamientos óptimos deben ser descubiertos explorando las distintas
posibilidades y observando sus resultados. Bajo estas condiciones, la evolución de
redes neuronales usando algoritmos genéticos, o neuroevolución, puede ser una
técnica muy poderosa.
2.4.1 Qué aporta la neuroevolución
Como ya hemos visto, al aumentar la complejidad de una red neuronal, aumenta
con ella el tamaño del espacio de búsqueda. Usando técnicas basadas en el
descenso del gradiente, como la retropropagación, provocamos que la función de
error obtenga un mayor número de mínimos locales esparcidos sobre una mayor
porción del espacio, dificultando el encontrar óptimos globales. Los algoritmos
genéticos, por el contrario, son particularmente buenos encontrando estos óptimos
globales en espacios de búsqueda grandes y complejos.
Otro punto fuerte de los algoritmos genéticos es su flexibilidad. Con pequeñas
modificaciones, pueden ser usados para entrenar desde redes neuronales
recurrentes hasta redes con funciones de activación discontinuas (y por tanto no
tratables con técnicas basadas en el gradiente). Como veremos a continuación,
incluso permiten optimizar no sólo los pesos, sino también la topología de la red.
Pero probablemente el tipo de generalización más importante que proporcionan
los algoritmos genéticos es el uso de una función de evaluación arbitraria, ya que
posibilita ideas como el uso de una función discontinua basada en el principio de
descripción de mínima longitud (MDL) [RISS89] para hacer que la red generalice
mejor. Esta característica resulta primordial a la hora de expandir las redes
neuronales más allá de tareas de regresión y clasificación, hacia dominios como la
lógica de control para vehículos autónomos o el procesamiento visual [MONT95].
33
2.4.2 Neuroevolución de topologías aumentativas (NEAT)
Hay una gran variedad de algoritmos neuroevolutivos. Se suelen distinguir dos
grupos principales: aquellos que sólo evolucionan los pesos sinápticos de una red
de topología predefinida, y aquellos que, además de los pesos, evolucionan
también la topología (Figura 13).
Figura 13 - Evolución de la topología de una red neuronal
La ventaja de las redes de topología estática es que el proceso de evolución es
mucho más sencillo. El precio a pagar es que el espacio de búsqueda de la
solución es también más limitado, y que nos vemos obligados a escoger una
topología que puede que no sea la mejor para resolver el problema.
Existen varios métodos que evolucionan la topología de la red además de los
pesos, pero en general tienen restricciones en cuanto al nivel de complejidad que
pueden alcanzar, y suelen empezar con topologías aleatorias. Un método que no
presenta estos inconvenientes, y que está atrayendo progresivamente la atención
de la comunidad investigadora, es NEAT (NeuroEvolution of Augmenting
Topologies) [NEAT06], desarrollado por Kenneth Owen Stanley y Risto
Miikkulainen en la Universidad de Texas [NNRG06].
34
La potencia de NEAT reside fundamentalmente en 3 aspectos:
• Utiliza marcadores históricos para posibilitar el cruce de redes con
topologías dispares, evitando además el problema de las representaciones
enfrentadas, en el que redes de idéntica funcionalidad son codificadas de
distinta forma, provocando efectos indeseados al recombinarlas entre sí
[HANC92].
• Incorpora un sistema de protección de las innovaciones genéticas mediante
el uso de especies.
• Partiendo de redes neuronales mínimas (sin nodos ocultos), realiza una
evolución incremental de su topología.
Supone asimismo una importante contribución en el campo de los algoritmos
evolutivos, ya que demuestra que es posible la obtención de soluciones cada vez
más óptimas a la par que complejas, reforzando la analogía con la evolución
biológica.
35
3 Descripción del
modelo desarrollado 3
Conociendo cómo funciona una red neuronal y un algoritmo genético, el concepto
de neuroevolución, y los fundamentos del paradigma NEAT, estamos en
condiciones de desarrollar nuestro componente. Analizaremos el genotipo y el
fenotipo de una red neuronal, los operadores aplicados durante el proceso de
evolución, y conceptos como las innovaciones o la gestión de especies.
Finalizado el componente, pasaremos crear un entorno de experimentación
adecuado para probarlo. Este entorno simulará un videojuego de acción en dos
dimensiones, en el que pondremos a prueba a los agentes en distintas situaciones.
Por último, trataremos de integrar nuestro componente en un videojuego real,
Half-Life 2, mediante el uso de plugins.
36
3.1 Implementación del paradigma NEAT
Para entender cómo funciona la implementación de NEAT que se ha hecho,
explicaremos la codificación de la red neuronal en forma de genes, su evolución
mediante los operadores de cruce y mutación, y la conservación de la innovación
mediante el uso de especies.
3.1.1 El genotipo en NEAT
El genoma usado en NEAT consiste básicamente de una lista de genes neurona y
otra de genes enlace. En la Figura 14 se tiene el diagrama UML de las clases y
estructuras usadas.
El gen neurona, NeuronGene, posee un identificador único, la función de la
neurona dentro de la red (entrada, salida, oculta, o bias), la pendiente de la función
de activación sigmoidal, y si es recurrente o no. En NEAT, una neurona se
considera recurrente si tiene algún enlace con origen y destino ella misma (Figura
15).
El gen enlace, LinkGene, contiene los identificadores de las dos neuronas que
conecta, el peso asociado al enlace, si está habilitado o no, si es recurrente o no, y
un identificador de innovación, cuya función se explicará más adelante.
En la Figura 16 se puede ver la codificación de una red neuronal cualquiera.
37
Figura 14 - Diagrama UML simplificado de la clase Genome, y de las estructuras
NeuronGene y LinkGene
Figura 15 - Neurona recurrente
38
5
4
1 2 3
Red (fenotipo)
Genes neurona:
Genes enlace:
ID:
Tipo:
ID:
Tipo:
ID:
Tipo:
ID:
Tipo:
ID:
Tipo:
2
Entrada
1
Entrada
3
Entrada
4
Oculta
5
Salida
Enlace:
Peso:
Activado:
Recurrente:
Innovación:
1 5
0.7
Sí
No
1
Enlace:
Peso:
Activado:
Recurrente:
Innovación:
2 5
-0.5
No
No
2
Enlace:
Peso:
Activado:
Recurrente:
Innovación:
2 4
0.6
Sí
No
3
Enlace:
Peso:
Activado:
Recurrente:
Innovación:
3 4
0.2
Sí
No
4
Enlace:
Peso:
Activado:
Recurrente:
Innovación:
4 5
0.4
Sí
No
8
Genoma (genotipo)
Figura 16 - Codificación de una red neuronal en NEAT (los genes sombreados están
desactivados)
39
3.1.2 Innovaciones
Una innovación sucede cada vez que se incorpora un gen neurona o un gen enlace
a la red, y es simplemente un registro de dicho cambio. Se mantiene un historial
de todas las innovaciones ocurridas con sus respectivos identificadores, y cada vez
que se añade un nuevo gen, se consulta. Si ya se había creado previamente esta
innovación, se le asigna al nuevo gen el identificador de dicha innovación. Si no,
se crea una nueva, y el gen es etiquetado con su identificador [STAN02]. En la
Figura 17 puede verse un diagrama UML con los elementos usados para llevar la
relación de innovaciones.
Figura 17 - Diagrama UML simplificado de la clase InnovationHistory, y de la estructura
Innovation
A modo de ejemplo, imaginemos que estamos evolucionando una red con dos
entradas y una salida, a la que añadimos una neurona, como se indica en la Figura
18. Al añadir la neurona 4 se producen tres innovaciones: una para la neurona, y
otras dos para los enlaces 1 4 y 4 3. El enlace 1 3 todavía existe en el
genoma, pero ha quedado desactivado.
40
Figura 18 - Innovaciones en una red neuronal
El historial de innovaciones antes de la mutación sería parecido al reflejado en la
Tabla 2.
ID Tipo Origen del enlace
Destino del enlace
ID de la neurona
Tipo de neurona
1 Neurona -1 -1 1 Entrada 2 Neurona -1 -1 2 Entrada 3 Neurona -1 -1 3 Salida 4 Enlace 1 3 -1 Ninguna 5 Enlace 2 3 -1 Ninguna
Tabla 2 - Historial de innovaciones antes de añadir la neurona
Después de la innovación, pasaría a ser el de la Tabla 3.
ID Tipo Origen del enlace
Destino del enlace
ID de la neurona
Tipo de neurona
1 Neurona -1 -1 1 Entrada 2 Neurona -1 -1 2 Entrada 3 Neurona -1 -1 3 Salida 4 Enlace 1 3 -1 Ninguna 5 Enlace 2 3 -1 Ninguna 6 Neurona 1 3 4 Oculta 7 Enlace 1 4 -1 Ninguna 8 Enlace 4 3 -1 Ninguna
Tabla 3 - Historial de innovaciones después de añadir la neurona
4
3
1
3
1 2 2
41
3.1.3 Operador de cruce
El operador de cruce en redes de topología dinámica puede ser muy problemático
(tanto que algunos métodos directamente lo descartan y funcionan únicamente con
mutaciones). Además de conseguir que el cruce de genomas de longitudes
distintas produzca redes válidas, se tiene que evitar la cuestión de las
representaciones enfrentadas ya citado.
En NEAT se afrontan ambos problemas usando los identificadores de innovación
como marcadores históricos de los genes. Dos genes con el mismo origen
histórico necesariamente tienen que representar la misma estructura, ya que ambos
derivaron del mismo gen ancestral en algún momento pasado. Esto es todo lo que
el sistema necesita para saber cómo recombinar los genomas.
Durante el cruce, los genes de los padres con el mismo identificador de
innovación se alinean. Estos son los llamados genes emparejables. Los genes con
distintos identificadores son denominados disjuntos o sobrantes, dependiendo de
si ocurren dentro o fuera del rango de innovaciones del otro padre, y representan
estructuras no comunes [STAN02].
Para crear el genoma hijo, los genes emparejables son escogidos aleatoriamente
de cualquiera de los padres, mientras que de los disjuntos y sobrantes sólo se
incluyen los del padre más apto. Si ambos tuviesen el mismo fitness, serían
heredados del genoma más corto (nos interesa que las redes sean tan sencillas
como sea posible). En caso de tener la misma longitud, también se escogerían
aleatoriamente. En la Figura 19 se supone que la red de la derecha muestra una
mayor aptitud.
42
Figura 19 - Cruzando dos redes (los genes sombreados están desactivados)
9
4
1 3 2
4
5
7
1 2 3
1
1 4
2
2 4
3
3 4
6
3 7
7
7 4
12
1 7
8
5 9
9
9 4
1
1 4
2
2 4
3
3 4
4
2 5
5
5 4
15
3 9
Padre 1:
Padre 2:
1
1 4
2
2 4
3
3 4
4
2 5
5
5 4
9
9 4
15
3 9 Hijo: 8
5 9
disjunto
disjunto disjunto sobrante
disjunto disjunto
disjunto
4
9
1 3
5
2
43
3.1.4 Operadores de mutación
Además del operador de cruce, esta implementación de NEAT cuenta con cuatro
operadores de mutación que realizan, según probabilidades configurables, las
operaciones que se describen seguidamente.
El primer operador que vamos a comentar es el encargado de añadir nuevas
neuronas. Para incorporar una nueva neurona a la red, primero se debe seleccionar
un enlace y deshabilitarlo. Se crean entonces dos enlaces nuevos para conectar la
neurona con sus vecinas. El peso del enlace deshabilitado se usa para uno de los
nuevos, y el otro se deja con el valor 1 (Figura 20). Añadir una neurona implica
por tanto tres innovaciones, como ya habíamos mencionado anteriormente.
El segundo operador a tratar es el responsable de añadir nuevos enlaces entre
neuronas. Puede crear tres tipos de enlaces (Figura 21):
• Un enlace hacia delante.
• Un enlace hacia atrás.
• Un enlace en bucle.
La función del tercer operador es la de modificar los pesos sinápticos de un enlace
entre dos neuronas. Puede hacerlo de dos formas:
• Sumando o restando al peso actual del enlace una cantidad aleatoria,
dentro de unos límites configurables.
• Sustituyendo el peso por otro totalmente nuevo.
Y el cuarto y último operador lo único que hace es modificar la pendiente de la
función de activación sigmoidal de una neurona, sumando o restando una cantidad
aleatoria, también dentro de unos límites adaptables por el usuario.
44
Figura 20 - Añadiendo una neurona
Figura 21 - Añadiendo un enlace
3.1.5 Gestión de especies
Cuando se añaden neuronas o enlaces al genoma, es bastante probable que el
nuevo individuo exhiba una peor aptitud hasta que tenga la oportunidad de
evolucionar y establecerse entre la población. Desgraciadamente, esto significa
que existe una alta probabilidad de que dicho individuo muera antes de demostrar
comportamientos potencialmente interesantes. Para preservar estas fuentes de
innovación durante los primeros momentos de su evolución, NEAT aplica el
concepto de gestión de especies: dividir la población según sus genes, de modo
que individuos similares sólo tengan que competir entre ellos y no con el resto,
quedando hasta cierto punto protegidos de una extinción prematura [STAN02].
La clase Species lleva el registro de las especies creadas. En cada generación, se
calcula la compatibilidad de todos los individuos de la población con el líder de
cada especie. Si dicha compatibilidad supera un cierto umbral, el individuo es
asignado a la misma. En la Figura 22 se tiene el diagrama UML de esta clase.
1.2
1.2
1
Hacia delante Hacia atrás Bucle
45
Figura 22 - Diagrama UML simplificado de la clase Species
Una vez que se asigna un individuo a una especie mediante el método
AddMember, sólo puede reproducirse con miembros de la misma especie. Sin
embargo, haciendo únicamente esto no protegemos la innovación. Tenemos que
encontrar un modo de ajustar la función objetivo de modo que ayude a los
individuos más jóvenes y diversos a mantenerse vivos durante un periodo de
tiempo razonable. En NEAT se premia a las especies más pequeñas y jóvenes, y
se castiga a las más grandes y viejas, usando multiplicadores del fitness ajustables.
De este modo resulta improbable que una sola especie se haga con toda la
población [STAN02]. Éste es el cometido del método AdjustFitness.
3.1.6 Qué ocurre en una época
Para tener una perspectiva global de todo lo visto hasta ahora, vamos a repasar lo
que ocurre en cada época del algoritmo genético. En la Figura 23 se puede ver el
diagrama UML de la clase GeneticAlgorithm, encargada de la manipulación de los
genomas, las innovaciones y las especies.
Al llamar al método Epoch, lo primero que sucede es que se eliminan todos los
fenotipos de la generación anterior. Después se examina cada especie, y se
eliminan todos sus miembros excepto el más apto. Además, si la especie no ha
mejorado su fitness desde hace un determinado número de generaciones,
desaparece.
46
Figura 23 - Diagrama UML simplificado de la clase GeneticAlgorithm
Hecho lo anterior, se evalúa la compatibilidad entre cada genoma y los
representantes de las especies que siguen vivas, y si es suficientemente alta se le
asigna a esa especie. Si no se encuentra ninguna especie compatible, se crea una
nueva compuesta únicamente por ese genoma. Una vez se han asignado todos los
genomas a alguna especie, se ajusta su fitness como se ha explicado
anteriormente.
Lo siguiente es calcular el número de hijos que debe generar cada especie,
basándose en el fitness de sus miembros, y en el fitness medio de la población.
Ahora recorremos cada especie generando individuos mediante cruce y
mutándolos si corresponde, hasta completar una nueva población.
3.1.7 Del genotipo al fenotipo
Ya sólo queda saber cómo se pasa del genotipo al fenotipo. En la Figura 24 se
puede ver el diagrama UML de las clases y estructuras usadas.
La clase NeuralNetwork es muy sencilla. Consiste simplemente en una colección
de neuronas que componen la red. Posee un método Render para representar la
estructura de la red en pantalla, y otro Update que actualiza la red a partir de las
entradas proporcionadas y devuelve la salida obtenida. El segundo parámetro
pasado a este método, type, requiere una explicación.
47
Figura 24 - Diagrama UML simplificado de la clase NeuralNetwork, y de las estructuras
Neuron y Link
Dado que en NEAT una red va a poder asumir cualquier topología, con enlaces
entre neuronas hacia delante, hacia atrás, o incluso consigo mismas, no se puede
usar un algoritmo de actualización normal basado en capas. Por eso, el método
Update permite dos tipos de funcionamiento:
• active: En este modo, los valores de activación viajan únicamente de una
neurona a la siguiente, en vez de por toda la red como ocurre usando
algoritmos convencionales. Para conseguir el mismo resultado que con
este tipo de algoritmos, tendríamos que repetir este proceso varias veces.
• snapshot: Usaremos este modo si, por el contrario, queremos que la
función se comporte como la de una red neuronal corriente. Para ello
tendremos que asegurarnos de actualizar todos los pesos desde la capa de
entrada hasta la de salida, por lo que iteraremos por cada neurona tantas
veces como profundidad tenga la red. Este tipo de funcionamiento es
apropiado si queremos usar un conjunto de entrenamiento.
Las neuronas vienen representadas por la estructura Neuron, que almacena un
identificador único, el tipo de neurona (entrada, salida, oculta o bias), la pendiente
48
de la función de activación sigmoidal, y la salida de la neurona. También tiene dos
vectores de enlaces, uno con los entrantes y otro con los salientes.
Los enlaces entre neuronas están representados por la estructura Link, que guarda
los punteros a las neuronas que conecta, el peso del enlace, y si se trata de un
enlace recurrente.
El método CreatePhenotype de la clase Genome es el encargado de recorrer el
genoma y crear las neuronas y los enlaces necesarios para formar una red
completa, la cual servirá de cerebro a nuestros personajes de la siguiente sección.
49
3.2 Desarrollo del entorno de
experimentación
Es el momento de probar nuestra implementación de NEAT. Para ello, se ha
creado un prototipo de videojuego de acción en 2D. Dados unos mapas
predefinidos, se añadirán agentes controlados por redes neuronales evolutivas, y
se intentará que cumplan una serie de objetivos. A continuación veremos cómo se
ha construido este entorno de experimentación, bautizado N*E*A*R
(NeuroEvolving Agents Redux).
Su código fuente se puede encontrar en el CD que acompaña a este trabajo. Son
unas 30.000 líneas de código, de las que la mitad pertenecen al motor de scripting
Lua [LUA_06] del que hace uso la aplicación. Véase el apéndice Contenido del
CD.
3.2.1 N*E*A*R
N*E*A*R es el entorno de experimentación desarrollado para probar la
implementación de NEAT que se ha hecho. Imita FPSs como Quake, pero en 2D
y con una vista cenital. En la Figura 25 se puede ver una pantalla de la aplicación.
El sistema de juego es muy básico, pero suficientemente complejo como para
probar los algoritmos que nos interesan. Los agentes son colocados en un mapa y
asignados a un equipo, y deben moverse esquivando obstáculos, disparando a los
agentes de equipos contrarios, y evitando ser disparados. La única forma que tiene
el usuario de interactuar con este mundo es mediante la colocación de banderas, a
las que los agentes tratarán de aproximarse.
50
Figura 25 - Pantalla de la aplicación
3.2.2 Arquitectura del juego
En la Figura 26 puede verse un diagrama UML de alto nivel de las clases que
conforman nuestro prototipo.
Figura 26 - Diagrama UML de alto nivel del entorno de experimentación
51
El eje de la arquitectura de N*E*A*R es la clase World, desde la que se controla
todo lo que ocurre en el mundo de juego. Analicemos sus partes.
3.2.3 Mapa
Un mapa de N*E*A*R es un conjunto de paredes que definen las habitaciones y
pasillos por las que se moverán los agentes. El mapa en sí está representado por la
clase Map, y las paredes por la clase Wall.
Los agentes nacen en posiciones específicas dentro del mapa, llamadas puntos de
encuentro. Estos puntos pueden pertenecer a un determinado equipo, de modo que
sólo los miembros de ese bando aparezcan ahí, o ser neutrales, en cuyo caso son
usados por cualquier agente. Están representados por la clase SpawnPoint.
En un mapa también puede haber torretas, pero hablaremos de ellas más adelante.
En la Figura 27 puede verse el diagrama UML de las clases que se han
mencionado.
Figura 27 - Diagrama UML simplificado de las clases usadas en un mapa
52
La información de cada mapa está almacenada en un archivo MAP, cuya
descripción detallada se encuentra en el apéndice Formato de los archivos MAP.
3.2.4 Entidades de juego
Para implementar las distintas entidades que aparecen en el mapa, y asegurarnos
de que la arquitectura sea fácilmente extensible, se han ideado tres clases
abstractas, BaseEntity, MovingEntity y ShootingEntity. Podemos verlas en el
diagrama UML de la Figura 28.
La clase BaseEntity es la raíz de la que derivan todas las entidades. Sus atributos
más importantes son id, un identificador único, type, que indica el tipo de entidad,
y position, que nos da su posición en el mapa. En cuanto a los métodos, Update es
llamado en cada instante o tick de juego para que la entidad se actualice según
corresponda, y Render dibuja la entidad en pantalla.
La clase MovingEntity hereda de BaseEntity, y añade la información necesaria
para simbolizar una entidad en movimiento. Sus atributos más importantes son
heading, que indica la dirección en la que la entidad se mueve, y facing, que
indica la dirección en la que mira (no tienen por qué coincidir).
La clase ShootingEntity, heredera de MovingEntity, se usará para toda aquella
entidad que pueda disparar armas. El atributo weapon apunta al arma equipada en
ese momento, score refleja la puntuación actual del agente, health su salud, status
si está vivo o muerto, y team el equipo al que pertenece. Los atributos fov y dov
los veremos aplicados más adelante, cuando hablemos de las torretas y los
agentes.
53
Figura 28 - Diagrama UML simplificado de las entidades del juego
3.2.5 Armas y proyectiles
Como acabamos de ver, las entidades de la clase ShootingEntity pueden llevar
armas, representadas a su vez por la clase abstracta BaseWeapon. De ella
heredarían todos los tipos de armas del juego. En esta versión sólo hay dos armas,
BlasterWeapon y MachineGunWeapon, pero la lista se podría ampliar fácilmente.
El método AimAt hace que el propietario del arma apunte al objetivo indicado por
target, mientras que ShootAt hace que le dispare.
54
Al disparar un arma, ésta crea un nuevo proyectil del tipo que corresponda. Todos
los proyectiles heredan de la clase abstracta BaseProyectile, que es a su vez hija
de MovingEntity. La clase World es la encargada de actualizar la posición de los
proyectiles llamando a su método Update, hasta que colisionan con otra entidad o
con una pared y mueren.
Cada agente empieza equipado con un arma de la clase BlasterWeapon, mientras
que las torretas usan armas de la clase MachineGunWeapon. Parámetros como la
munición máxima del arma, su velocidad de disparo, y demás, son configurables
por el usuario.
En la Figura 29 se tiene el diagrama UML de las clases implicadas.
55
Figura 29 - Diagrama UML simplificado de las clases usadas para implementar las armas del
juego
3.2.6 Torretas
Las torretas son entidades de la clase ShootingEntity, cuya única función es
disparar a todo agente enemigo que entre en su campo de visión. Este campo de
visión viene definido por dos atributos a los que se ha hecho referencia
56
anteriormente: fov y dov. El primero indica el ángulo de visión que tiene la
entidad, y el segundo la distancia hasta la que puede ver. El esquema de la Figura
30 muestra ambos conceptos.
Ángulo de visión
Distancia
Figura 30 - Representación del campo de visión de la torreta
Cuando un agente enemigo entra en el campo de visión de la torreta, es guardado
en la variable target, y hasta que no salga del mismo o muera, la torreta no
buscará otro objetivo. También puede usarse una torreta para patrullar una zona,
estableciendo los extremos de su trayectoria con los atributos from y to. En la
Figura 31 se tiene el diagrama UML de la clase Turret.
Figura 31 - Diagrama UML simplificado de la clase Turret
57
3.2.7 Agentes
Una de las tareas que van a tener que realizar los agentes es navegar por el mapa
esquivando los obstáculos a su paso. Para ello, el agente debe ser capaz de:
• Observar la geometría del mapa.
• Llevar a cabo las acciones necesarias para evitar colisiones potenciales.
Para que el agente sea capaz de percibir los obstáculos, hemos decidido dotarle de
antenas o sensores que se originan en su centro y cubren un ángulo de 180º. Tanto
el número de sensores como su longitud son configurables por el usuario.
También usamos el concepto de radio de colisión, que es el del círculo que
engloba toda la geometría de la entidad. En la Figura 32 se puede verse un agente
con cinco sensores de obstáculos, y su radio de colisión.
0.940.54
-1 -1
-1
Figura 32 – Sensores de obstáculos
En cada tick de juego se comprueba si existe alguna intersección entre los
sensores del agente y las paredes del mapa. Si es así, se anota la distancia a la que
se produjo el impacto, normalizada entre 0 y 1 (0 sería el centro del agente, 1 el
extremo del sensor). De no existir intersección en un sensor, se anota -1.
Existe otro sensor relacionado con la navegación, que sólo toma los valores 0 y 1,
y que se activa cuando un obstáculo entra en el radio de colisión del agente.
Aunque podría parecer que con los sensores de obstáculos descritos arriba el
agente tendría información más que suficiente, mediante experimentación ha
58
quedado demostrado que el uso de este sensor adicional mejora la eficacia del
algoritmo.
El otro cometido de los agentes es atacar a los enemigos y evitar sus disparos, por
lo que deben ser capaces de:
• Detectar la presencia de miembros de equipos contrarios.
• Apuntar y dispararles.
• Saber dónde están apuntando e intentar no ponerse a tiro.
Para abordar este problema, se ha seguido el mismo planteamiento que con los
obstáculos, sólo que, en vez de cubrir 180º con los sensores, se cubre el campo de
visión del agente. Véase la Figura 33.
-1-1
-1
-1
0.96
Figura 33 - Sensores de enemigos
Como ocurría antes, anotamos para cada sensor la distancia a la que se ha
detectado algún enemigo. Además de estos sensores, usaremos otros tres: uno
para advertir al agente que tiene un enemigo en el punto de mira, y dos para
indicarle la dirección de disparo del enemigo más cercano.
Las lecturas de estos sensores servirán de entradas a la red neuronal que gobernará
las acciones del agente. Sus salidas serán cuatro: moverse hacia delante y hacia
atrás, moverse a izquierda y derecha, mirar a izquierda y derecha, y disparar. En la
Figura 34 se tiene una representación de la red resultante, similar a la empleada
en el proyecto NERO (NeuroEvolving Robotic Operatives) [NERO06, STAN05].
59
Moverse adelante/atrás
Moverse izq./der.
Disparar
Mirar izq./der.
Figura 34 - Red neuronal de un agente
Uno de los aspectos clave del algoritmo genético que evolucionará los pesos y
topología de estas redes es la función objetivo que escojamos. En nuestro caso,
hemos tenido en cuenta:
• Distancia a las banderas: El usuario puede colocar marcas en el mapa, y
los agentes que más se acerquen a ellas recibirán mayor fitness. De este
modo conseguimos controlar parcialmente hacia donde se mueven los
agentes.
• Puntuación: Con el fin de estimular el combate entre los equipos, por cada
disparo que un agente acierte a un enemigo, recibirá puntos que se
traducirán en un mayor fitness.
• Munición: Para evitar el gasto innecesario de munición, cuanta menos
utilicen, mayor fitness les corresponderá.
• Salud: Nos interesa que los agentes aprendan a esquivar los disparos
enemigos, por lo que recompensaremos a aquellos que acaben la época
con más salud.
Cada uno de estos puntos tiene asociado un multiplicador entre -1 y 1, de manera
que podemos castigar o premiar comportamientos según nos interese.
Sensores de enemigos
Sensores de obstáculos
Detector de colisiones
Línea de fuego
Bias
Topología evolutiva
Objetivo a la vista
60
Para finalizar, en la Figura 35 se puede ver el diagrama UML de la clase Bot.
Figura 35 - Diagrama UML simplificado de la clase Bot
3.2.8 Visión global del mundo
Como ya hemos dicho, la clase World es la que gobierna todo lo que ocurre en el
juego. Cuando el usuario carga un mapa, se crean los genomas de una nueva
población de individuos con sus correspondientes fenotipos. Se asignan las redes
neuronales resultantes a los agentes, y se les añade al mapa.
En cada tick del juego, el método Update de la clase World actualizará las torretas
del mapa, y todos los proyectiles que se encuentren activos. Si todavía no se ha
cumplido el tiempo que dura una generación, actualizará también los agentes
(sensores, posición, etc.). De haberse cumplido, calculará el fitness de cada
agente, y llamará al método Epoch del algoritmo genético para que realice las
operaciones de selección, cruce y mutación, dando lugar a una nueva población.
Sustituirá entonces los cerebros antiguos de los agentes por los nuevos,
comenzando de nuevo el proceso.
61
En la Figura 36 se tiene un diagrama UML de la clase World.
Figura 36 - Diagrama UML simplificado de la clase World
62
3.3 Integración en Half-Life 2
Habiendo probado con éxito nuestro componente en el entorno de
experimentación desarrollado, es hora de intentar integrarlo en un juego real, más
concretamente en Half-Life 2, FPS de última generación desarrollado por Valve
Software.
Para ello, crearemos un plugin que nos permita añadir agentes (comúnmente
llamados bots en el ámbito de los videojuegos) y aplicarles nuestro algoritmo
neuroevolutivo, como hemos venido haciendo en nuestra plataforma N*E*A*R.
Haremos uso del SDK que proporciona la propia Valve, y de la documentación
que encontremos tanto en el wiki oficial [VDEV06a] como en otras páginas
dedicadas a estos temas [BUNT06, INTR06, MDDB06].
El código fuente del plugin también se puede encontrar en el CD que acompaña a
este trabajo. Véase el apéndice Contenido del CD.
3.3.1 Creación del plugin
En la Figura 37 se puede observar la arquitectura que sigue Half-Life 2 para la
utilización de plugins.
Figura 37 - Arquitectura de plugins de Half-Life 2
Sistema Operativo
Plugin Plugin Plugin
Half-Life 2
Motor Source
63
Los plugins de Half-Life 2 funcionan exponiendo la interfaz
IServerPluginCallbacks al motor Source, que se encarga de llamar a sus distintos
métodos según los eventos de servidor que tengan lugar [VDEV06b].
El método que más nos interesa es GameFrame. Es llamado una vez por cada tick
del servidor (típicamente 60 veces por segundo), por lo que cumplirá la misma
función que el método Update de la clase World en nuestro entorno de
experimentación: actualizar los sensores de los agentes, hacer que se muevan y
disparen, y, en caso de completarse una generación, llamar al método Epoch de la
clase GeneticAlgorithm para obtener una nueva población.
En la Figura 38 vemos un diagrama UML de nuestro plugin con las interfaces que
debe implementar.
64
Figura 38 – Diagrama UML simplificado de nuestro plugin de Half-Life 2
3.3.2 Bots en Half-Life 2
Una funcionalidad que podríamos echar de menos para nuestros agentes de Half-
Life 2 es que fuesen capaces de capturar eventos de juego, como el envío de
mensajes o la muerte de algún jugador. Esto es posible implementando la interfaz
IGameEventListener, que consta de un único método, FireGameEvent. Puede
verse en el diagrama UML de la Figura 39.
65
Figura 39 - Diagrama UML simplificado de nuestro bot de Half-Life 2
El control de los personajes es bastante sencillo. Cada una de las acciones que son
capaces de realizar tiene una potencia de 2 asociada (atacar es 20, moverse hacia
delante es 210, saltar es 220, etc.), de modo que en cada tick de juego podemos
hacer un XOR de todas las que necesitemos, y almacenar el resultado en el
atributo actions. Pasándole este valor al método RunPlayerMove, declarado en la
interfaz IBotController, Half-Life 2 se encargará de que el personaje lleve dichas
acciones a cabo.
66
La percepción del mundo es algo más complicada. Se basa en el uso de trazas,
líneas que van de un punto del mundo a otro, y que son capaces de informarnos
del primer objeto con el que se encuentran. El método UTIL_TraceLine recibe,
entre otros parámetros, el punto de origen y destino de la traza, y devuelve una
instancia de la clase trace_t con información detallada de los resultados
obtenidos: punto en el que ha impactado con algún objeto, porcentaje de la
distancia total que se ha recorrido hasta el momento del impacto, entidad con la
que se ha impactado, equipo al que pertenece, y varios parámetros más
[VDEV06c].
Tenemos entonces todo lo que necesitamos para nuestros bots. Usaremos trazas
que salgan del cuerpo del agente, y con la información que nos proporcionen
alimentaremos su red neuronal.
3.3.3 El resultado
En la Figura 40 puede verse una captura de pantalla de Half-Life 2: Deathmatch
en la que aparece uno de nuestros bots.
Figura 40 – Uno de nuestros bots dentro de Half-Life 2: Deathmatch
67
Un contratiempo con el que nos hemos encontrado es que sólo se pueden añadir
dos agentes al juego, ya que al añadir el tercero se produce un error. Como no
hemos sido capaces de encontrar el fallo en nuestro código, intentamos ponernos
en contacto con Valve Software para ver cuál podría ser la causa, pero no hemos
recibido respuesta. Esto imposibilita la utilización de nuestro algoritmo
neuroevolutivo, porque con una población tan pequeña no se pueden obtener
resultados de ningún tipo.
Aún así, los agentes se comportan de la forma esperada dentro de los mapas de
Half-Life 2: Deathmatch y Counter-Strike: Source, por lo que, de solucionar el
problema al que se alude arriba, sería factible la implantación de nuestro
componente en el juego.
68
4 Análisis de
resultados 4
Habiendo completado el entorno de desarrollo, podemos probar la capacidad de
nuestro componente para elaborar estrategias complejas, y que permitan a los
personajes del videojuego adaptarse a los cambios de su entorno. Para ello se
dispone de varios mapas, cada uno de ellos creado con el fin de demostrar
distintas capacidades de los agentes.
Desgraciadamente, el error encontrado a la hora de integrar el componente en
Half-Life 2 nos impedirá hacer ningún tipo de estudio sobre su comportamiento
en un juego real.
69
4.1 En el entorno de experimentación
Pasamos a explicar los resultados obtenidos en nuestros mapas, que mostrarán la
habilidad de los agentes en aspectos como la navegación por un área con
obstáculos, la detección de enemigos, o el combate con otras entidades.
Las simulaciones llevadas a cabo para cada mapa han sido grabadas en video, y se
pueden encontrar en el CD que acompaña a este trabajo. Véase el apéndice
Contenido del CD.
4.1.1 Obstáculos (I)
El primer mapa en el que vamos a probar el comportamiento de los agentes,
obst_1.map, es muy simple. Nacerán en la parte de abajo del mapa, y tendrán que
encontrar la forma de llegar a un punto situado en la parte de arriba. Entre ambos
hay un muro, que deberán aprender a sortear. Véase la Figura 41.
Figura 41 - Mapa con obstáculos (I)
70
Los parámetros de configuración más importantes de este mapa son:
• Tamaño de la población: 50 individuos
• Sensores de obstáculos: 5
• Tasa de supervivencia: 0,2
• Tasa de cruce: 0,7
• Tasa de mutación: 0,15
• Probabilidad de añadir una neurona: 0,04
• Probabilidad de añadir un enlace: 0,07
• Probabilidad de añadir un enlace recurrente: 0,05
• Probabilidad de reemplazar un peso: 0,1
• Perturbación máxima de los pesos: 0,5
• Perturbación máxima de la pendiente de la función de activación: 0,1
• Número máximo de especies: 5
• Umbral de compatibilidad: 0,26
Sólo necesitamos los sensores de obstáculos y el detector de colisiones, ya que no
hay enemigos en el mapa. Por eso las redes tendrán únicamente siete neuronas en
la capa de entrada: cinco sensores, el detector, y una neurona bias.
Después de 20 generaciones, se observaron unos resultados lo suficientemente
satisfactorios, y se detuvo la ejecución del programa. En la Figura 42 puede verse
información sobre la población, en la Figura 43 una gráfica con el progreso del
fitness a lo largo de las generaciones (la línea azul indica el mejor fitness para esa
generación, mientras que la roja indica el fitness medio de la población), y en la
Figura 44 los cuatro mejores fenotipos de la última generación.
71
Figura 42 - Información sobre la población
Figura 43 - Progreso del fitness
Figura 44 - Mejores fenotipos
72
En la Figura 45 podemos ver una captura de pantalla con indicaciones del
comportamiento exhibido por los agentes durante las últimas generaciones. Se
observaron dos tendencias claras: un grupo que se movía hacia la izquierda y
usaba la pared como guía para llegar hasta el objetivo, frente a otro que rodeaba el
muro para luego dirigirse directamente al punto verde.
Figura 45 - Comportamiento de los agentes
Viendo el número de generaciones que se ha tardado en encontrar soluciones
aceptables, y la estructura de los mejores fenotipos de la última generación (una
de las redes ni siquiera tiene neuronas ocultas), podemos asegurar que este mapa
no supone ningún reto para nuestros agentes.
4.1.2 Obstáculos (II)
El mapa obst_2.map añade otro muro, y un estrechamiento a mitad de camino
hacia el objetivo. Véase la Figura 46.
73
Figura 46 - Mapa con obstáculos (II)
Los parámetros usados fueron los mismos que en el apartado anterior. Después de
30 generaciones se detuvo la ejecución. En la Figura 47 puede verse información
sobre la población, en la Figura 48 la gráfica del fitness, y en la Figura 49 los
cuatro mejores fenotipos.
Figura 47 - Información sobre la población
74
Figura 48 - Progreso del fitness
Figura 49 - Mejores fenotipos
Durante las primeras generaciones parecían incapaces de alcanzar el objetivo,
hasta que en la séptima vuelta uno de los individuos esquivó todos los obstáculos
correctamente (se puede apreciar en la gráfica del fitness un salto en la línea azul).
A partir de ahí no tuvieron demasiados problemas, escogiendo la mayoría el
camino representado en la Figura 50.
Tras estos experimentos queda claro que, usando los algoritmos neuroevolutivos
que proporciona nuestro componente, navegar por un mapa esquivando obstáculos
no es ningún desafío.
75
Figura 50 - Comportamiento de los agentes
4.1.3 Torretas fijas
Este mapa, fix_1.map, incluye torretas además de obstáculos, como se puede
observar en la Figura 51. Los agentes parten de la zona inferior del mapa, y deben
llegar a la superior. Para ello, deberán aprender a detectar la amenaza de las
torretas y tomar el camino de la izquierda, en vez de seguir recto hacia una muerte
segura.
Aunque las torretas pueden disparar, los agentes no dispondrán de munición. No
nos interesa que las ataquen, sólo que las eviten. Estudiaremos comportamientos
de combate en experimentos subsiguientes.
76
Figura 51 - Mapa con torretas fijas
Los parámetros más importantes de este mapa son los siguientes:
• Tamaño de la población: 50 individuos
• Sensores de obstáculos: 5, repartidos en 180º
• Sensores de enemigos: 5, repartidos en 120º
• Tasa de supervivencia: 0,2
• Tasa de cruce: 0,7
• Tasa de mutación: 0,2
• Probabilidad de añadir una neurona: 0,04
• Probabilidad de añadir un enlace: 0,07
• Probabilidad de añadir un enlace recurrente: 0,05
• Probabilidad de reemplazar un peso: 0,1
• Perturbación máxima de los pesos: 0,5
• Perturbación máxima de la pendiente de la función de activación: 0,1
• Número máximo de especies: Sin límite
• Umbral de compatibilidad: 0,26
77
En este caso sí que necesitamos todos los sensores: cinco sensores de obstáculos,
el detector de colisiones, otros cinco sensores de enemigos, el indicador de
objetivos, y el de la línea de fuego enemiga. Si añadimos el bias, tenemos un total
de quince neuronas en la capa de entrada. Comparando con el espacio de
búsqueda de los mapas anteriores, podemos predecir que en éste va a ser bastante
más difícil encontrar soluciones satisfactorias.
Aunque se simularon más de 160 generaciones, los resultados no mejoraron
significativamente tras la 95. En la Figura 52 puede verse información sobre la
población, en la Figura 53 la gráfica del fitness, y en la Figura 54 los cuatro
mejores fenotipos.
Figura 52 - Información sobre la población
Figura 53 - Progreso del fitness
78
Figura 54 - Mejores fenotipos
Durante las 50 primeras generaciones, la mayoría de los agentes se dirigía a la
derecha y, o bien se paraban al llegar a la pared, o intentaban subir y morían a
causa de las torretas. Alrededor de la generación 60 uno de los individuos, al
detectar las torretas, decidió girar a la izquierda, encontrando el camino hacia el
objetivo. En generaciones sucesivas se fue incrementando el número de agentes
que escogían este camino, recorriéndolo cada vez más eficientemente.
En paralelo, sin embargo, otro grupo de agentes seguía una pauta muy distinta.
Viendo que los que avanzaban hacia la derecha tendían a morir, estos se quedaban
parados en la primera pared que encontraban. Este comportamiento podría
corregirse haciendo uso de los multiplicadores ya mencionados al hablar de la
función de fitness de los agentes. Dando menos peso a la salud, conseguiríamos
que fuesen un poco más “atrevidos”.
En la Figura 55 pueden verse indicados ambos comportamientos.
79
Figura 55 - Comportamiento de los agentes
4.1.4 Torretas móviles
En el mapa mob_1.map nos vamos a centrar exclusivamente en el combate. Los
agentes nacerán en la parte inferior del mapa, y en cuanto intenten subir se
encontrarán con un muro patrullado por dos torretas móviles. Deberán atacarlas a
la vez que evitan recibir daño.
80
Figura 56 - Mapa con torretas móviles
Los parámetros de configuración son los mismos que en el mapa anterior.
Después de 50 generaciones se detuvo la ejecución, por no observar mejoras
significativas en el comportamiento de los agentes. En la Figura 57 puede verse
información sobre la población, en la Figura 58 la gráfica del fitness, y en la
Figura 59 los cuatro mejores fenotipos.
Figura 57 - Información sobre la población
81
Figura 58 - Progreso del fitness
Figura 59 - Mejores fenotipos
A partir de la generación 20 se empieza a ver el comportamiento que luego
seguirá la mayoría de individuos: se acercan al muro y, en el momento en que
detectan una torreta, empiezan a dispararla a la vez que se mueven en sentido
contrario, distanciándose de ella. Cuando el objetivo ha salido de su campo de
visión, se vuelven a acercar, y repiten el proceso (Figura 60). Esta técnica,
consistente en disparar al enemigo mientras te alejas de él andando de espaldas, es
muy común entre jugadores humanos en FPSs, y se conoce como backpedaling.
82
Figura 60 - Comportamiento de los agentes
Resulta muy estimulante observar lo elaborado de las estrategias que pueden
desarrollar los agentes sin intervención alguna por nuestra parte.
4.1.5 Otros agentes
El último mapa con el que vamos a probar nuestro componente, bots_1.map,
enfrenta dos equipos de agentes. El equipo azul empezará en la parte superior del
mapa, y el rojo en la inferior, quedando separados por un muro. Véase la Figura
61.
83
Figura 61 - Mapa con agentes de equipos distintos
Los parámetros de configuración utilizados son los mismos que en los dos mapas
anteriores. Se ejecutó la simulación durante 100 generaciones. En la Figura 62
puede verse información sobre la población, en la Figura 63 la gráfica del fitness,
y en la Figura 64 los cuatro mejores fenotipos.
Figura 62 - Información sobre la población
84
Figura 63 - Progreso del fitness
Figura 64 - Mejores fenotipos
Tras unas primeras generaciones un poco caóticas, los dos equipos decidieron
encontrarse en la parte derecha del mapa. El equipo azul se mostró en general más
agresivo, sobrepasando el muro y persiguiendo a miembros del equipo rojo, los
cuales, al encontrarse frente a un oponente, repetían la técnica de backpedaling
vista anteriormente. Algunos agentes azules llegaron incluso a valerse de una de
las esquinas del muro para cubrirse del fuego enemigo mientras disparaban. Véase
la Figura 65.
85
Figura 65 - Comportamiento de los agentes
Otra técnica observada en la simulación, y que también es muy usada por
jugadores humanos, es el circle strafing: moverse en círculos alrededor del
enemigo mientras le disparas.
El hecho de que los agentes sean capaces de producir por sí solos estrategias tan
complejas como las observadas pone de manifiesto el buen funcionamiento de
nuestro componente, y lo provechoso que puede llegar a ser el uso de redes
neuronales evolutivas en el control de personajes en videojuegos.
86
4.2 En Half-Life 2
Como hemos explicado, el límite en el tamaño de la población encontrado al
integrar nuestro componente en Half-Life 2 imposibilita cualquier estudio acerca
de su funcionamiento. Aún así, en el CD que acompaña a este trabajo se tienen
videos de los agentes moviéndose por mapas de Half-Life 2: Deathmatch y
Counter-Strike: Source. Véase el apéndice Contenido del CD.
87
5 Planificación 5
El proyecto fue dividido en cinco paquetes de trabajo principales (Figura 66):
• Gestión del proyecto
• Realización de la memoria
• Desarrollo del componente.
• Construcción del entorno de experimentación.
• Integración del componente en Half-Life 2.
Figura 66 - Paquetes de trabajo del proyecto
88
Los paquetes de gestión del proyecto y realización de la memoria, WP-01 y WP-
02 respectivamente, no requieren mayor explicación. El paquete WP-03 engloba
el desarrollo del componente que hemos venido describiendo a lo largo del
trabajo, aplicando las ideas del paradigma de neuroevolución NEAT. Está
dividido en varios paquetes, como se ve en la Figura 67.
Figura 67 - Desarrollo del componente
El paquete WP-04 es el que recoge la construcción del prototipo de videojuego 2D
a modo de entorno de experimentación para probar nuestro componente. Puede
verse en la Figura 68.
Figura 68 - Entorno de experimentación
89
Y por último, el paquete WP-05, en el que se realizan las tareas de integración del
componente en Half-Life 2. Se divide a su vez en otros paquetes, como muestra la
Figura 69.
Figura 69 - Integración en Half-Life 2
En la Figura 70 puede verse un diagrama de Gantt con la planificación del
proyecto, iniciado alrededor del 4 de julio de 2005, y finalizado para el 31 de
agosto de 2006.
90
Figura 70 - Planificación del proyecto
IdN
ombr
e de
tare
aD
urac
ión
11
Ges
tión
del p
roye
cto
14 m
ss2
2 Re
aliz
ació
n de
la m
emor
ia4
mss
33
Desa
rrol
lo d
el c
ompo
nent
e70
día
s4
3.1
Inve
stiga
ción
ace
rca
de la
neu
roev
oluc
ión
4 se
m5
3.2
Impl
emen
taci
ón d
e NE
AT50
día
s6
3.2.
1 M
odel
ado
de u
na re
d ne
uron
al3
sem
73.
2.2
Cod
ifica
ción
gen
étic
a de
la re
d ne
uron
a4
sem
83.
2.3
Cre
ació
n de
l alg
oritm
o ge
nétic
o3
sem
94
Ento
rno
de e
xper
imen
taci
ón50
día
s10
4.1
Inte
rfaz
gráf
ica
2 se
m11
4.2
Mov
imie
nto
de lo
s ag
ente
s4
sem
124.
3 Co
mba
te c
on o
tras
entid
ades
4 se
m13
5 In
tegr
ació
n en
Hal
f-Life
220
0 dí
as14
5.1
Fam
iliar
izac
ión
con
el m
otor
Sou
rce
50 d
ías
155.
1.1
Cre
ació
n de
un
plug
in4
sem
165.
1.2
Con
trol d
e la
s ac
cion
es2
sem
175.
1.3
Per
cepc
ión
del m
undo
4 se
m18
5.2
Inco
rpor
ació
n de
l com
pone
nte
al v
ideo
jueg
o6
sem
1921
1009
0705
0402
0130
2826
2725
2422
07 a
go '
05 ju
n '0
603
abr
'06
30 e
ne '0
628
nov
'05
26 s
ep '0
525
jul '
05ay
'05
m
91
6 Valoración
económica 6
Los gastos incurridos en el proyecto se deben principalmente a las horas
dedicadas. Si estimamos el sueldo de un programador en 12€ por hora, y teniendo
en cuenta que el proyecto nos ha llevado unas 550 horas, obtendríamos un coste
de desarrollo del componente de 6600€.
En cuánto a licencias, sólo se han requerido tres:
• Windows XP Professional Edition: ~85€
• Visual Studio .NET 2003: ~800€
• Half-Life 2: ~55€
Como vimos, la producción de un juego moderno cuesta entre $800.000 y casi $2
millones. Los algoritmos de inteligencia artificial del mismo bien pueden suponer
un 20% del tiempo total dedicado, lo que serían entre $160.000 y $400.000. El
hecho de tener un componente reutilizable que permita implementar en todos
nuestros proyectos agentes con las características descritas, y cuyo precio sea
menos de la décima parte de lo que le cuesta a la compañía hacerlo por su cuenta,
sin duda lo hace sumamente atractivo.
92
7 Conclusiones 7
Finalizamos este trabajo detallando los objetivos cumplidos:
1. Investigación acerca de la neuroevolución
El campo de la inteligencia artificial en los videojuegos modernos es sumamente
exigente. La cantidad de acciones posibles para los agentes en cada instante de
juego es enorme. Tradicionalmente se han venido usando técnicas deterministas
como las máquinas de estados, los sistemas expertos o los árboles de decisión, lo
que lleva a que los personajes controlados por el ordenador converjan a
comportamientos muy similares. El usuario aprende a reconocer estos patrones
rápidamente, y el juego pasa a ser predecible y aburrido.
Utilizando técnicas estocásticas evitamos este fenómeno, consiguiendo que el
jugador se encuentre constantemente con nuevas experiencias y retos. Sin
embargo, hallar soluciones adecuadas en un espacio de búsqueda de semejante
tamaño es una tarea que presenta numerosos problemas, entre otros el tiempo y la
capacidad de proceso requeridos, y el hecho de que nos podemos quedar
estancados en un óptimo local sin llegar a alcanzar el global.
La neuroevolución resuelve en gran parte estas cuestiones, combinando las
ventajas de los algoritmos genéticos y las redes neuronales. De los primeros
93
aprovechamos su aptitud para recorrer todo el espacio de búsqueda sin distraerse
en mínimos locales, mientras que de las segundas obtenemos una forma óptima de
codificar la solución. Eso sí, arrastramos también algunos de sus inconvenientes,
como el gran número de parámetros a configurar, o la dificultad que conlleva
depurar este tipo de soluciones.
2. Desarrollo del componente
Para el desarrollo de nuestro componente estudiamos el paradigma NEAT, el cual
nos permite evolucionar tanto la topología de las redes neuronales como sus
pesos, librándonos de tener que escoger una estructura que puede que no sea la
más apropiada para nuestro problema. Pero lo que lo hace verdaderamente único
es que, partiendo de una red totalmente conectada y sin neuronas ocultas,
consigue llegar a soluciones cada vez más óptimas y complejas. Para ello aplica
unos ingeniosos operadores de cruce y mutación basados en marcadores históricos
para ir añadiendo nodos y enlaces, y hace uso del concepto de gestión de especies
para conservar la innovación a lo largo de las generaciones.
Nuestra implementación de NEAT posibilita el aplicar la potencia y flexibilidad
de este paradigma en cualquier área, dado su carácter modular y altamente
configurable.
3. Creación del prototipo de videojuego
El prototipo de videojuego de acción en 2D que creamos a modo de entorno de
experimentación cumplió perfectamente con su cometido. Nos permitió probar las
capacidades de nuestros agentes, los cuales produjeron estrategias relativamente
elaboradas: encontraban la forma de esquivar los distintos obstáculos del mapa,
detectaban y atacaban a sus oponentes, e incluso hacían uso de técnicas propias de
jugadores avanzados, como el backpedaling o el circle strafing.
Vistos los resultados de estos experimentos, nuestro componente ha evidenciado
su idoneidad en el ámbito de los videojuegos.
94
4. Familiarización con el motor Source
Creamos un plugin de Half-Life 2, que nos permitió añadir bots tanto a Half-Life
2: Deathmatch como a Counter-Strike: Source. Además fuimos capaces de
controlar sus acciones mediante código, consiguiendo entre otras cosas que un
jugador humano lo dirigiese mediante mensajes de texto dentro del juego.
También logramos percibir los objetos en el entorno del bot mediante trazas,
imitando la idea de los sensores de obstáculos usada en nuestro prototipo.
5. Integración del componente en Half-Life 2
Al tratar de incorporar poblaciones de más de dos agentes en los mapas de Half-
Life 2, nos encontramos con un error que imposibilitó llevar a cabo un estudio
completo del funcionamiento de nuestro componente en un videojuego real. No
obstante, una vez solucionado este problema, no hay duda de que los resultados
tan positivos observados en el entorno de experimentación se repetirían aquí.
95
8 Bibliografía 8
[BUCK02] M. Buckland. AI Techniques for Game Programming. Premier
Press. Octubre 2002.
[BUNT06] Bots United. 6 Julio 2005.
<http://www.bots-united.com/>
[GAME05a] “Report from Japan: Xbox development to cost $$$”. Gamespot.
12 Agosto 2005. 19 Noviembre 2005.
<http://www.gamespot.com/news/6130901.html>
[GAME05b] “Game development costs to double?”. Gamespot. 19 Septiembre
2005. 19 Noviembre 2005.
<http://www.gamespot.com/news/6133848.html>
[GMAI06] The Game AI Page. 20 Noviembre 2005.
<http://www.gameai.com/>
[GSTR03a] “AI Middleware: Getting into Character, Part 1”. Gamasutra. 21
Julio 2003. 20 Noviembre 2005.
<http://www.gamasutra.com/features/20030721/dybsand_01.shtml>
[GSTR03b] “AI Middleware: Getting into Character, Part 2”. Gamasutra. 22
Julio 2003. 20 Noviembre 2005.
<http://www.gamasutra.com/features/20030722/dybsand_01.shtml>
[GSTR03c] “AI Middleware: Getting into Character, Part 3”. Gamasutra. 23
Julio 2003. 20 Noviembre 2005.
<http://www.gamasutra.com/features/20030723/dybsand_01.shtml>
96
[GSTR03d] “AI Middleware: Getting into Character, Part 4”. Gamasutra. 24
Julio 2003. 20 Noviembre 2005.
<http://www.gamasutra.com/features/20030724/dybsand_01.shtml>
[GSTR03e] “AI Middleware: Getting into Character, Conclusion”. Gamasutra.
25 Julio 2003. 20 Noviembre 2005.
<http://www.gamasutra.com/features/20030725/dybsand_01.shtml>
[HANC92] P. Hancock. “Genetic Algorithms and Permutation Problems: A
Comparison of Recombination Operators for Neural Net Structure
Specification”. Proceedings of the International Workshop on
Combinations of Genetic Algorithms and Neural Networks (COGANN
‘92). 1992.
[HL2_06] Half-Life 2. 23 Junio 2005.
<http://www.half-life2.com/>
[INTR06] Interlopers.net. 17 Julio 2005.
<http://www.interlopers.net/>
[LUA_06] The Programming Language Lua. 15 Octubre 2005.
<http://www.lua.org/>
[MDDB06] Mod DB. 17 Julio 2005.
<http://www.moddb.com/>
[MONT95] D. J. Montana. “Neural Network Weight Selection Using Genetic
Algorithms”. 1995.
[NEAT06] NEAT: Evolving Increasingly Complex Neural Network
Topologies. 1 Mayo 2006.
<http://nn.cs.utexas.edu/project-
view.php?RECORD_KEY(Projects)=ProjID&ProjID(Projects)=14>
[NERO06] NERO: NeuroEvolving Robotic Operatives. 2 Mayo 2006.
<http://www.nerogame.org/>
[NNRG06] UTCS Neural Networks Research Group. 1 Mayo 2006.
<http://nn.cs.utexas.edu/>
[RISS89] J. Rissanen. “Stochastic Complexity in Statistical Inquiry”. World
Scientific. 1989.
[SCHW04] B. Schwab. AI Game Engine Programming. Charles River Media.
Septiembre 2004.
97
[SOAR06] Soar: An Architecture for Human Cognition. 20 Noviembre 2005.
<http://sitemaker.umich.edu/soar/>
[STAN02] K. O. Stanley, R. Miikkulainen. “Eficient Reinforcement Learning
through Evolving Neural Network Topologies”. Proceedings of the
Genetic and Evolutionary Computation Conference (GECCO ‘02). 2002.
[STAN05] K. O. Stanley, B. B. Bryant, R. Miikkulainen. “Evolving Neural
Network Agents in the NERO Video Game”. Proceedings of the IEEE
2005 Symposium on Computational Intelligence and Games (CIG ‘05).
2005.
[VALV06] Valve Software. 23 Junio 2005.
<http://www.valvesoftware.com/>
[VDEV06a] Valve Developer Community. 6 Julio 2005.
<http://developer.valvesoftware.com/>
[VDEV06b] “Server Plugins”. Valve Developer Community. 6 Julio 2005.
<http://developer.valvesoftware.com/wiki/Server_Plugins>
[VDEV06c] “Trace Lines”. Valve Developer Community. 17 Julio 2005.
<http://developer.valvesoftware.com/wiki/TraceLines>
[WOOD01] S. Woodcock. “Game AI: The State of the Industry”. Game
Developer Magazine. Agosto 2001.
98
9 Apéndices 9
99
9.1 Glosario
AI : Artificial Intelligence // Inteligencia Artificial
ANN : Artificial Neural Network // Red Neuronal Artificial
CD : Compact Disc // Disco Compacto
CPU : Central Processing Unit // Unidad Central de Proceso
DNA : Deoxyribonucleic Acid // Ácido Desoxirribonucleico
DOV : Distance of View // Distancia de Visión
FOV : Field of View // Campo de Visión
FPS : First Person Shooter // Juego de Acción en Primera Persona
FSM : Finite State Machine // Máquina de Estados Finita
FuSM : Fuzzy State Machine // Máquina de Estados Borrosa
GA : Genetic Algorithm // Algoritmo Genético
MDL : Minimum Description Length // Descripción de Mínima
Longitud
NE : Neuroevolution // Neuroevolución
N*E*A*R : Neuroevolving Agents Redux // Vuelta a los Agentes
Neuroevolutivos
NEAT : Neuroevolution of Augmenting Topologies // Neuroevolución
de Topologías Aumentativas
NERO : Neuroevolving Robotic Operatives // Agentes Robóticos
Neuroevolutivos
NN : Neural Network // Red Neuronal
SDK : Software Development Kit // Equipo de Desarrollo de Software
UML : Unified Modeling Language // Lenguaje Unificado de
Modelado
100
9.2 Manual de usuario de N*E*A*R
La aplicación consta únicamente del ejecutable near.exe, no depende de ninguna
librería externa. Al abrirlo, nos encontraremos con una pantalla como la de la
Figura 71.
Figura 71 - Pantalla inicial de N*E*A*R
En la parte superior se encuentra la barra de menú, que permite al usuario realizar
distintas acciones que se detallarán más adelante. Debajo de ella hay una barra de
herramientas con iconos representando las acciones que consideramos son las más
usadas. El área blanca que ocupa la mayor parte de la ventana es el lienzo sobre el
que se dibujarán el mapa y las distintas entidades de juego.
El primer menú empezando por la izquierda es File, que sólo tiene dos elementos
(Figura 72):
• Open: Permite cargar un mapa en formato MAP.
• Exit: Cierra la aplicación.
101
Figura 72 - Menú File
Si elegimos Open, aparecerá un diálogo para escoger el mapa que deseemos,
como se ve en la Figura 73. Podemos conseguir esto mismo desde la barra de
herramientas ( ).
Figura 73 - Diálogo de selección del mapa
Una vez seleccionado el archivo, si tiene el formato correcto, será mostrado en el
lienzo de la ventana. Los agentes nacerán de sus puntos de encuentro y empezarán
a moverse por el mapa. Puede verse en la Figura 74.
102
Figura 74 - Mapa cargado correctamente
El punto verde que se ve cerca del centro del mapa es una bandera. Para colocar
una, sólo hay que hacer clic derecho en el punto del mapa que se quiera, y los bots
incrementarán su fitness según se acerquen a ella. Para quitarla, basta con volver a
hacer clic derecho encima.
Sigamos con los menús. El siguiente, Game, tiene estos elementos (Figura 75):
• Pause: Pausa el juego. Se puede activar y desactivar desde la barra de
herramientas ( ).
• Fast Forward: Hace que el juego se ejecute todo lo rápido que permita el
procesador. Tiene su propio icono en la barra de herramientas ( ).
• Render: Indica si se debe o no dibujar el mapa y sus entidades en el lienzo.
Cuando queramos simular muchas generaciones del algoritmo genético, y
no necesitemos ver lo que ocurre en todo momento en el mundo, podemos
activar la opción Fast Forward y desactivar Render para reducir el tiempo
de espera. También es accesible desde la barra de herramientas ( ).
103
Figura 75 - Menú Game
Dentro del menú View tenemos varios submenús. El primero, Map, contiene estos
elementos (Figura 76):
• Wall Normals: Muestra los vectores normales a las paredes del mapa. Sólo
es una ayuda visual para que el diseñador pueda comprobar que todo está
colocado correctamente.
• Spawn Points: Representa en el lienzo los puntos de encuentro de cada
equipo, en su color correspondiente. De ellos nacen los agentes al
comienzo de cada generación.
104
Figura 76 - Menú View/Map
El segundo submenú, Bot, contiene estos elementos (Figura 77):
• Bounding Radius: Dibuja el radio de colisión de los agentes. Si un
obstáculo entra en contacto con este círculo, se activará el detector de
colisiones del agente.
• Vision Field: Muestra el campo de visión de los agentes.
• Object Sensors: Representa los sensores de objetos de los agentes. Si un
sensor está de color verde claro, indica que se ha producido una
intersección con alguna pared. Si por el contrario está verde oscuro, no ha
detectado ningún obstáculo.
• Enemy Sensors: Representa los sensores de enemigos de los agentes. Igual
que pasa con los sensores de objetos, si alguno está de color verde claro ha
descubierto un enemigo, mientras que si está verde oscuro no ha detectado
nada.
105
Figura 77 - Menú View/Bot
El tercer submenú, Turret, contiene estos otros elementos (Figura 78):
• Bounding Radius: Dibuja el radio de colisión de las torretas.
• Vision Field: Muestra el campo de visión de las torretas.
106
Figura 78 - Menú View/Turret
Debajo de estos submenús, el menú View ofrece más elementos. El primero de
ellos, Population Info, nos da información sobre la población de la generación
anterior a la actual (Figura 79). Tiene varios campos:
• Generation: Número de la generación para la que se muestra la
información.
• Average Fitness: Valor de fitness medio de la población en esta
generación.
• Best Fitness: Mejor fitness de entre todos los individuos de esta
generación.
• Best Ever Fitness: Mejor fitness encontrado hasta el momento, teniendo en
cuenta todas las generaciones.
• Species: Número de especies en las que está dividida la población.
• Threshold: Umbral de compatibilidad usado a la hora de determinar si un
individuo pertenece a una especie.
• Distribution: Muestra un gráfico representando la distribución de
individuos entre las distintas especies.
107
Figura 79 - Diálogo Population Info
El siguiente elemento, Fitness Graph, presenta una gráfica con el progreso del
fitness hasta la generación anterior a la actual. El eje horizontal representa las
generaciones, y el vertical el fitness. La línea azul corresponde al mejor fitness de
cada generación, y la roja al fitness medio. Nos permite conocer, de un vistazo, la
evolución que han experimentado los individuos con el paso del tiempo. Se
muestra una gráfica de ejemplo en la Figura 80.
Figura 80 - Diálogo Fitness Graph
El último elemento del menú View es Best Phenotypes, que nos muestra los
mejores fenotipos de la generación anterior a la actual (Figura 81). El número de
redes a representar es configurable por el usuario, como se puede ver en el
apéndice Formato de los archivos MAP.
108
Las neuronas siempre son rojas, pero los enlaces, según su tipo, se dibujan de un
color u otro:
• Amarillo: Enlace hacia delante con un peso inhibidor (menor o igual que
cero).
• Gris: Enlace hacia delante con peso excitador (mayor que cero).
• Azul: Enlace hacia atrás o en bucle, y con peso inhibidor.
• Rojo: Enlace hacia atrás o en bucle, con peso excitador.
• Verde: Enlace que sale de la neurona bias.
Figura 81 - Diálogo Best Phenotypes
Para terminar sólo nos queda el menú Help, que contiene el elemento About
N*E*A*R. Al seleccionarlo se nos mostrará un diálogo con información sobre la
versión y la fecha de compilación del programa (Figura 82).
109
Figura 82 - Diálogo About N*E*A*R
110
9.3 Formato de los archivos MAP
Un archivo MAP es simplemente un archivo de texto con una lista de elementos a
añadir al mapa. Sólo existen tres tipos de elementos: paredes, torretas y puntos de
encuentro. Veamos como se declara cada uno.
Para crear una pared, escribiríamos una línea como la siguiente:
1 <x1> <y1> <x2> <y2> <x3> <y3>
El 1 es la constante que identifica este elemento como una pared. Los valores
<x1> e <y1> serían las coordenadas de origen de la pared, mientras que <x2> e
<y2> serían las de destino. Los valores <x3> e <y3> indican el vector normal a
esta pared.
Para crear una torreta, la línea a añadir tendría este aspecto:
2 <x1> <y1> <x2> <y2> <x3> <y3>
De nuevo, el 2 es la constante que identifica el elemento como una torreta. Los
valores <x1> e <y1> serían el origen del movimiento de la torreta, y <x2> e
<y2> el destino. Si queremos una torreta fija, sólo tenemos que escribir el mismo
punto para ambos. Los valores <x3> e <y3> indican la dirección en la que mira
la torreta al empezar (si la torreta es móvil, dejará de mirar en esta dirección en
cuanto comience a desplazarse).
Y para crear un punto de encuentro, escribiríamos algo así:
3 <x1> <y1> <t>
El 3 identifica el elemento como un punto de encuentro. Los valores <x1> e
<y1> nos dan su posición, y <t> el equipo al que pertenece.
111
Antes de declarar los elementos hay que añadir una línea con el ancho y alto del
mapa:
<ancho> <alto>
Y eso es todo. Veamos un mapa de ejemplo, basic.map:
560 560
1 20 20 540 20 0 1
1 540 20 540 540 -1 0
1 540 540 20 540 0 -1
1 20 540 20 20 1 0
1 140 270 420 270 0 -1
1 420 270 420 290 1 0
1 420 290 140 290 0 1
1 140 290 140 270 -1 0
2 60 60 280 60 1 0
2 500 500 280 500 -1 0
3 280 80 1
3 280 480 2
El aspecto que tendría este mapa es el de la Figura 83. La primera línea nos indica
que es un mapa de 560 píxeles de ancho por 560 de alto. Las siguientes ocho
líneas, al empezar por 1, sabemos que representan las paredes que delimitan el
mapa y el muro del centro. Las que empiezan por 2 son dos torretas: una de ella
patrulla desde el punto (60, 60) hasta el (280, 60), y la otra lo hace desde (500,
500) hasta (280, 500). Por último, vemos que hay dos líneas que empiezan por 3,
lo que nos dice que declaran puntos de encuentro: el primero se encuentra en la
parte de arriba del mapa, en (280, 80), y pertenece al equipo azul, mientras que el
segundo se encuentra en la parte de abajo, en (280, 480), y pertenece al equipo
rojo.
112
x
y
Figura 83 - Mapa de ejemplo
Todo archivo .map va acompañado de otro de configuración con el mismo
nombre, pero acabado en .ini. En él se indican los siguientes parámetros:
• Parámetros generales:
o Deathmatch: Indica si se trata de un mapa en el que se vayan a
enfrentar dos equipos (si es así, los agentes se dividirán entre el
equipo azul y el rojo, en caso contrario serán asignados todos al
azul).
• Parámetros de configuración de bots y torretas:
o NumBots: Número de agentes que forman la población.
o NumBestBots: Cuántos de los mejores fenotipos se deben
almacenar (y que luego se mostrarán en la ventana de diálogo Best
Phenotypes).
o BotMass: Masa del agente (afecta a su velocidad).
o BotScale: Escala del agente, por si necesitamos redimensionarlo.
o BotMaxSpeed: Máxima velocidad a la que se pueden mover los
agentes.
113
o BotMaxForce: Máxima fuerza de empuje que pueden experimentar
los agentes.
o BotMaxTurnRate: Máxima velocidad de giro de los agentes.
o BotMaxHealth: Máxima salud que puede tener un agente.
o BotFOV: Ángulo de visión de los agentes.
o BotDOV: Distancia de visión de los agentes.
o BotObjectSensorNum: Número de sensores de objetos de los
agentes.
o BotObjectSensorLength: Longitud de los sensores de objetos.
o BotCollidedSensor: Número de detectores de colisión de los
agentes (sólo debe tomar los valores 0 o 1).
o BotEnemySensorNum: Número de sensores de enemigos de los
agentes.
o BotEnemySensorLength: Longitud de los sensores de enemigos.
o BotOnTargetSensor: Número de detectores de objetivos (sólo debe
tomar los valores 0 o 1).
o BotEnemyLOFSensor: Número de detectores para la línea de fuego
enemiga (sólo debe tomar los valores 0 o 2).
o TurretMass: Masa de la torreta (afecta a su velocidad).
o TurretScale: Escala de la torreta, por si queremos redimensionarla.
o TurretMaxSpeed: Máxima velocidad de la torreta.
o TurretMaxForce: Máxima fuerza de empuje que pueden
experimentar las torretas.
o TurretMaxTurnRate: Máxima velocidad de giro de las torretas.
o TurretFOV: Ángulo de visión de la torreta.
o TurretDOV: Distancia de visión de la torreta.
• Parámetros de configuración de las armas:
o BlasterAmmo: Munición con la que empiezan los blaster.
o BlasterMaxAmmo: Máxima munición que puede llevar un blaster
(si es 0, la munición para esta arma es infinita).
o BlasterRateOfFire: Tasa de disparo del blaster.
o BlasterProjectileDamage: Daño producido por un proyectil de
blaster.
o BlasterProjectileMass: Masa de un proyectil de blaster.
114
o BlasterProjectileMaxSpeed: Máxima velocidad de un proyectil de
blaster.
o BlasterProjectileMaxForce: Máxima fuerza que puede
experimentar un proyectil de blaster.
o MachineGunAmmo: Munición con la que empiezan las
ametralladoras.
o MachineGunMaxAmmo: Máxima munición que puede llevar una
ametralladora (si es 0, la munición para esta arma es infinita).
o MachineGunRateOfFire: Tasa de disparo de la ametralladora.
o MachineGunProjectileDamage: Daño producido por un proyectil
de ametralladora.
o MachineGunProjectileMass: Masa de un proyectil de
ametralladora.
o MachineGunProjectileMaxSpeed: Máxima velocidad de un
proyectil de ametralladora.
o MachineGunProjectileMaxForce: Máxima fuerza que puede
experimentar un proyectil de ametralladora.
• Parámetros de configuración del algoritmo genético:
o NumNetworkInputs: Número de entradas de las redes neuronales
que hacen de cerebro de los agentes.
o NumNetworkOutputs: Número de salidas de dichas redes.
o NumTicks: Número de ticks de juego que dura una generación.
o SurvivalRate: Tasa de supervivencia de las especies.
o CrossoverRate: Tasa de cruce.
o MutationRate: Tasa de mutación.
o YoungAgeThreshold: Edad hasta la que se considera que una
especie es joven.
o YoungAgeMultiplier: Multiplicador del fitness para especies
jóvenes (normalmente será mayor que 1, para intentar proteger a
las especies que acaban de entrar en juego).
o OldAgeThreshold: Edad a partir de la cual se considera que una
especie es vieja.
115
o OldAgeMultiplier: Multiplicador del fitness para especies viejas
(normalmente será menor que 1, para penalizar aquellas especies
que lleven más tiempo en juego).
o MaxPermittedNeurons: Máximo número de neuronas que puede
tener una red.
o AddNeuronProbability: Probabilidad de añadir una neurona.
o NumTriesToFindOldLink: Número de intentos para encontrar un
enlace (usado en el algoritmo encargado de añadir neuronas a un
genoma).
o AddLinkProbability: Probabilidad de añadir un enlace.
o AddRecurrentLinkProbability: Probabilidad de añadir un enlace
hacia atrás o en bucle.
o NumTriesToFindLoop: Número de intentos para encontrar un bucle
(usado en el algoritmo encargado de añadir enlaces a un genoma).
o NumTriesToAddLink: Número de intentos para añadir un enlace
(usado en el algoritmo encargado de añadir enlaces a un genoma).
o ReplaceWeightProb: Probabilidad de reemplazar por completo un
peso.
o MaxWeightPerturbation: Máxima perturbación que puede sufrir un
peso.
o MaxActivationPerturbation: Máxima perturbación que puede sufrir
la pendiente de una función de activación.
o MaxNumberOfSpecies: Máximo número de especies (si se pone a
0, no hay límite).
o CompatibilityThreshold: Valor inicial del umbral de
compatibilidad de especies.
o NumGensAllowedWithoutImprovement: Número de generaciones
que permitimos vivir a una especie que no muestra ninguna
mejoría en su fitness.
El mapa anterior, basic.map, iría acompañado de un archivo de configuración
basic.map.ini similar a éste:
116
-- [[ general game parameters ]] --
Deathmatch = true
-- [[ bot parameters ]] --
NumBots = 50
NumBestBots = 4
BotMass = 1.0
BotScale = 7.0
BotMaxSpeed = 1.0
BotMaxForce = 0.05
BotMaxTurnRate = 0.05
BotMaxHealth = 100
BotFOV = 120.0
BotDOV = 160.0
BotObjectSensorNum = 5
BotObjectSensorLength = 30.0
BotCollidedSensor = 1
BotEnemySensorNum = 5
BotEnemySensorLength = BotDOV
BotOnTargetSensor = 1
BotEnemyLOFSensor = 2
TurretMass = 1.0
TurretScale = 10.0
TurretMaxSpeed = 1.0
TurretMaxForce = 0.05
TurretMaxTurnRate = 0.05
TurretFOV = 100.0
TurretDOV = 200.0
-- [[ weapons ]] --
BlasterAmmo = 100
BlasterMaxAmmo = 100
BlasterRateOfFire = 1.0
117
BlasterProjectileDamage = 10
BlasterProjectileMass = 1
BlasterProjectileMaxSpeed = 3.0
BlasterProjectileMaxForce = 100.0
MachineGunAmmo = 0
MachineGunMaxAmmo = 0
MachineGunRateOfFire = 5.0
MachineGunProjectileDamage = 50
MachineGunProjectileMass = 1
MachineGunProjectileMaxSpeed = 4.0
MachineGunProjectileMaxForce = 100.0
-- [[ genetic algorithm ]] --
NumNetworkInputs = BotObjectSensorNum + BotCollidedSensor + BotEnemySensorNum + BotOnTargetSensor + BotEnemyLOFSensor
NumNetworkOutputs = 4
NumTicks = 1000
SurvivalRate = 0.2
CrossoverRate = 0.7
MutationRate = 0.2
YoungAgeThreshold = 10
YoungAgeMultiplier = 1.3
OldAgeThreshold = 50
OldAgeMultiplier = 0.7
MaxPermittedNeurons = 100
AddNeuronProbability = 0.04
NumTriesToFindOldLink = 5
AddLinkProbability = 0.07
AddRecurrentLinkProbability = 0.05
NumTriesToFindLoop = 5
118
NumTriesToAddLink = 5
ReplaceWeightProb = 0.1
MaxWeightPerturbation = 0.5
MaxActivationPerturbation = 0.1
MaxNumberOfSpecies = 0
CompatibilityThreshold = 0.26
NumGensAllowedWithoutImprovement = 15
El archivo de configuración es procesado por el motor de scripting Lua 5.0.2, por
lo que cualquier expresión válida en este lenguaje también lo es en el archivo.
119
9.4 Creación e instalación de un plugin de
Half-Life 2
Lo primero es obtener el Source SDK, para lo cual arrancaremos la aplicación
Steam (incluida al instalar Half-Life 2), seleccionaremos la pestaña Tools, y
haremos doble clic sobre Source SDK (Figura 84).
Figura 84 - Steam
Después de descargarse las actualizaciones pertinentes, nos aparecerá la ventana
de la Figura 85, en la que haremos doble clic sobre Create a Mod.
120
Figura 85 - Source SDK
Si fuésemos a crear una modificación de Half-Life 2 para un único jugador, en la
pantalla de la Figura 86 seleccionaríamos Modify Half-Life 2 Single Player. En
nuestro caso, sin embargo, lo que nos interesa es crear una modificación
multijugador, por lo que seleccionamos Modify Half-Life 2 Multiplayer.
Figura 86 - Tipo de modificación que vamos a llevar a cabo
121
Por último, nos preguntará el directorio donde queremos que se descompriman
todos los archivos de la SDK, y el nombre de nuestra modificación (Figura 87).
Cuando termine de copiar los archivos, cerraremos esa ventana.
Figura 87 - Información de nuestra modificación
Ahora vamos a compilar uno de los plugins que trae el SDK a modo de ejemplo.
Nos movemos hasta el directorio C:\Mods\src\utils\serverplugin_sample\ y
hacemos doble clic sobre el archivo serverplugin_empty.vcproj (doy por hecho
que se tiene Visual Studio .NET 2003 instalado). Hacemos clic derecho sobre el
proyecto serverplugin_empty, y seleccionamos Properties. Dentro del apartado
Custom Build Step, introduciremos en Command Line las siguientes líneas
(sustituyendo usuario por el nombre de usuario de nuestra cuenta de Steam):
attrib -r "C:\Archivos de programa\Valve\Steam\SteamApps\ usuario\half-life 2 deathmatch\bin\serverplugin_empty.dll"
copy "$(TargetPath)" "C:\Archivos de programa\Valve\Steam\ SteamApps\usuario\half-life 2 deathmatch\bin\ serverplugin_empty.dll"
Y en Outputs, ésta otra (sustituyendo de nuevo usuario por nuestro nombre de
usuario):
"C:\Archivos de programa\Valve\Steam\SteamApps\usuario\ half-life 2 deathmatch\bin\serverplugin_empty.dll"
122
Si el plugin, en vez de ser de Half-Life 2: Deathmatch, fuese de Counter-Strike:
Source, deberíamos modificar las rutas anteriores de modo que apuntasen a la
carpeta bin adecuada. En Command Line introduciríamos:
attrib -r "C:\Archivos de programa\Valve\Steam\SteamApps\ usuario\counter-strike source\bin\serverplugin_empty.dll"
copy "$(TargetPath)" "C:\Archivos de programa\Valve\Steam\ SteamApps\usuario\counter-strike source\bin\ serverplugin_empty.dll"
Y en Outputs:
"C:\Archivos de programa\Valve\Steam\SteamApps\usuario\ counter-strike source\bin\serverplugin_empty.dll"
Puede verse la pantalla correspondiente en la Figura 88. Otra cosa a tener en
cuenta es que estos directorios sólo existen si se ha ejecutado el juego al que
corresponden por lo menos una vez.
Figura 88 - Modificando la configuración del proyecto
Aprovechemos para configurar las opciones del apartado Debugging, de modo
que podamos usar el propio Visual Studio para depurar nuestro plugin. Si
trabajamos con Half-Life 2: Deathmatch, en Command deberemos introducir:
123
"C:\Archivos de programa\Valve\Steam\SteamApps\usuario\ half-life 2 deathmatch\hl2.exe"
Y en Command Arguments, esto otro:
-game "C:\Archivos de programa\Valve\Steam\SteamApps\ usuario\half-life 2 deathmatch\hl2mp\" -windowed -dev -allowdebug
De tratarse de Counter-Strike: Source, en Command pondríamos:
"C:\Archivos de programa\Valve\Steam\SteamApps\usuario\ counter-strike source\hl2.exe"
Y en Command Arguments:
-game "C:\Archivos de programa\Valve\Steam\SteamApps\ usuario\counter-strike source\cstrike\" -windowed -dev -allowdebug
Se muestra la pantalla correspondiente en la Figura 89. De este modo, al
seleccionar Debug/Start de la barra de menú de Visual Studio, se lanzará
automáticamente Half-Life 2: Deathmatch o Counter-Strike: Source, deteniéndose
la ejecución en los breakpoints que hayamos puesto y permitiéndonos ver paso a
paso lo que sucede en el código.
Al ejecutar el proyecto, veremos una pantalla como la de la Figura 90. En la
consola que aparece, vamos a escribir el comando plugin_print para comprobar
que no hay ningún plugin cargado. Ahora, si introducimos el comando
plugin_load serverplugin_empty, cargará el módulo indicado. Podemos
comprobar que todo ha funcionado correctamente volviendo a escribir
plugin_print. Este proceso es común para todo plugin que queramos usar en Half-
Life 2.
124
Figura 89 - Modificando las opciones de depuración
Figura 90 - Cargando el plugin en Half-Life 2: Deathmatch
Si queremos que un plugin se cargue automáticamente al arrancar el juego, lo
único que tenemos que hacer es crear un archivo con la extensión .vdf en el
directorio C:\Archivos de programa\Valve\Steam\SteamApps\usuario\half-life 2
125
deathmatch\hl2mp\addons\ para Half-Life 2: Deathmatch, o C:\Archivos de
programa\Valve\Steam\SteamApps\usuario\counter-strike source\cstrike\addons\
para Counter-Strike: Source y escribir en él:
"Plugin"
{
"file" "serverplugin_empty"
}
Si volvemos a ejecutar el proyecto y escribimos plugin_print, veremos que el
plugin ya se encuentra cargado.
En la Figura 91 y en la Figura 92 puede verse el plugin desarrollado para este
proyecto ejecutándose en Half-Life 2: Deathmatch y en Counter-Strike: Source,
respectivamente.
Figura 91 - N*E*A*R en Half-Life 2: Deathmatch
126
Figura 92 - N*E*A*R en Counter-Strike: Source
127
9.5 Contenido del CD
El CD que acompaña a este trabajo tiene la siguiente estructura de directorios:
• hl2:
o bin:
nearplugin.dll: Plugin desarrollado para integrar nuestro
componente en Half-Life 2.
o src:
nearplugin: Carpeta que contiene el código fuente del
plugin.
nearplugin.sln: Solución de Visual Studio .NET 2003 para
cargar el proyecto.
• near:
o bin:
maps: Carpeta con mapas para el entorno de
experimentación N*E*A*R.
near.exe: Ejecutable de N*E*A*R.
o src:
lua: Carpeta con el código fuente del motor de scripting
Lua.
near: Carpeta con el código fuente de N*E*A*R.
near.sln: Solución de Visual Studio .NET 2003 para cargar
el proyecto.
• videos:
o hl2:
css.avi: Video que muestra bots de Counter-Strike: Source
controlados por nuestro plugin.
hl2dm.avi: Video que muestra bots de Half-Life 2:
Deathmatch controlados por nuestro plugin.
128
o near:
bots_1.avi: Video correspondiente al experimento descrito
en el apartado Otros agentes.
fix_1.avi: Video correspondiente al experimento descrito en
el apartado Torretas fijas.
mob_1.avi: Video correspondiente al experimento descrito
en el apartado Torretas móviles.
obst_1.avi: Video correspondiente al experimento descrito
en el apartado Obstáculos (I).
obst_2.avi: Video correspondiente al experimento descrito
en el apartado Obstáculos (II).
• perez06.pdf: Copia de este trabajo en formato digital.
• readme.txt: Descripción del contenido del CD.
129