Vida Artificial

  • Uploaded by: fernando sanchez
  • 0
  • 0
  • May 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Vida Artificial as PDF for free.

More details

  • Words: 7,309
  • Pages:
Tutorial de Arte y Vida Artificial Movimiento Se explican cuestiones básicas de movimiento bidimensional. Así como la generación de agentes autónomos, capaces de desarrollar su propia historia. También se estudia la generación de un espacio toroidal. En este apartado se verá como construir agentes independientes que simulen el comportamientos de organismos. Aquí sólo trataremos el tema del movimiento y nuetra finaidad es lograr tener organismos virtuales que sean capaces de manejarse en forma autónoma y que expongan cierta naturalidad en su movimiento. también veremos como implementar el espacio de la escena de la mejor forma para aprovechar los recursos del sistema.

Organismos y autonomía Como ya se ha dicho, aquí trataremos la creación de varios organismos independientes unos de otros. La autonomía de estos organismos se logra a través de la creación de una clase Organismo, la cual permite crear las variables internes necesarias para tener registro de la evolución de cada individuo por separado. A la hora de programar necesitamos que tratamientos homogéneos a la hora del diseño, manifiesten autonomía e independencia a la hora de la ejecución. esto se logra creando un arreglo de objetos de tipo Organismo. Así desde el código principal del programa se les pide a cada organismo las mismas acciones, pero cada organismo responde a estas ordenes según su propia historia. A continuación podemos ver el código de la clase ogranismo: class Organismo{ float x, y; //posicion del organismo Organismo( float x_ , float y_ ){ //inicializa el organismo iniciar( x_ , y_ ); } Organismo( ){ //inicializa el organismo iniciar( random(width) , random(height) ); //si no recibe parametros inicia con x e y al azar } void iniciar( float x_ , float y_ ){ //inicialización del organismo x = x_; y = y_; } void dibujar(){ //dibuja el organismo rectMode(CENTER); rect(x,y,10,10); } } En esta primera implementación de la clase Organismo, un organismo posee las variables necesarias para ubicarlas en un espacio bidimensional (x e y). Por el momento lo único que pueden hacer nuestro organismos es ocupar un lugar en el espacio. El código que ejecuta las acciones se muestra a continuación (los puntos suspensivos indican que existe código que no está siendo mostrado.): Organismo[] animales; //Un arreglo de animales int cantAnimales; //Define la cantidad de animales void setup(){ ... cantAnimales = 20; //Se establece la cantidad de animales iniciar(); //ejecuta la inicialización …

void draw(){ ... for(int i=0;i
Como muestra el ejemplo anterior: 1) la cantidad de organismos es de 20, que es el valor de la variable cantAnimale. 2) Dos ciclos for son los encargados de ordenar los comportamientos de los organismos, por ejemplo, cuando se les envía a inicializarse con el constructor de la clase ( animales[ i ] = new Organismo( ) ), o cuando se les pide que se dibujen ( animales[ i ].dibujar( ) ). Como dijimos anteriormente, las ordenes generales tiene un tratamiento homogéneo, por ejemplo se les pide a todos los organismos, que se dibujen; pero la forma en que cada organismo responde a esta acción depende de su propia historia. En este caso la historia está dado por la inicialización, que determina una posición al azar: … Organismo( ){ //inicializa el organismo iniciar( random(width) , random(height) ); //si no recibe parametros inicia con x e y al azar } void iniciar( float x_ , float y_ ){ //inicialización del organismo x = x_; y = y_; } ...

Movimiento y desplazamiento Una vez que hemos ubicado nuestro organismos en diferentes posiciones, es momento de que cada organimo se mueva. la forma de implementar este movimiento, es declarando dos variable dx y dy que representan los desplazamentos en x e y, respectivamente. Así se les asigna valor al azar al inicio, para luego ser usadas en la acción mover:

class Organismo{ ... float dx,dy; //desplazamiento en x e y ... void iniciar( float x_ , float y_ ){ //inicialización del organismo x = x_; y = y_; dx = random(-10,10); dy = random(-10,10); } void mover(){ //actualiza la ubicación del organismo x += dx; //aplica los desplazamiento y += dy; //aplica los desplazamiento

...

}

El problema que surge en este ejempo es que la velocidad varía según la conjunción de los valores de dx y dy. Además, los valores de estos desplazamiento, nos dificulta prever la dirección exacta en la que avanzará un organismo. Seria deseable poder controlar la velocidad independientemente de la dirección, y viceversa.

Coordenadas polares y rectangulares Cuando hablamos de movimiento en informática tenemos que hablar de la representación del movimiento y por ende tenemos que hablar del sistema de coordenadas que se utiliza. El sistema de Coordenadas Cartesianas (ver en Wikipedia), también llamado Coordenadas Rectangulares, permite utilizar las distancia con respecto a dos ejes (X e Y) para definir un punto en el espacio bidimensional. Este sistema de coordenadas es muy útil para representar posiciones. Nos resulta intuitivo trabajar con este sistema sin embargo no nos es tan útil a la hora de pensar movimientos, dado que en general, a la hora de pensar el movimiento, pensamos en dirección y velocidad. Afortunadamente existe un tipo de cordenadas que pueden representar este tipo de variables, son las Coordenadas Polares. En este sistema, un punto en el espacio se representa como la distancia respceto a un punto de origen, medida como un ángulo y una distancia . Debido a que la computadora domina un sistema de Coordenadas Rectangulares, pero a nuestro fin, nos es más útil el sistema de Coordenadas Polares, es necesario contar con un sistema de transformación de un sistema a otro. Las ecuaciones que permiten dicha transformación son las siguientes:

  x = distancia * cos( angulo ); y = distancia * sin( angulo );

En el ejemplo Vida 02, el ángulo está representado por la dirección y la distancia por la velocidad. En vez de x e y se emplean dx y dy, dado que en este caso, las primeras representan la posición mientras que dx y dy los respectivos desplazamientos en cada uno de estos ejes. Así en el ejemplo la operación sería: dx = velocidad * cos(direccion); dy = velocidad * sin(direccion); x += dx; y += dy;

De esta forma, se agregaron en la clase organismo las variables y operaciones necesarios para trabajar con velocidad y dirección: class Organismo{ ... float x, y; //posicion del organismo float direccion; //dirección en la que avanza float velocidad; //velocidad a la que avanza float dx,dy; //desplazamiento en x e y deducido de la // dirección y velocidad. ... void iniciar( float x_ , float y_ ){ //inicialización del organismo x = x_; y = y_; direccion = random(TWO_PI); //inicia con una dirección al azar velocidad = 5; //inicia la velocidad en 5 pixels por fotograma

} void mover(){ //actualiza la ubicación del organismo dx = velocidad * cos(direccion); //deduce el desplazamiento en X dy = velocidad * sin(direccion); //deduce el desplazamiento en Y x += dx; //aplica los desplazamiento y += dy; //aplica los desplazamiento } ...

Variación azarosa de dirección En este ejemplo se ha agregado un función que permite hacer una variación angular de la dirección. Es decir, en cada paso se varía levemente la dirección para que la trayectoria de nuetros organismo sea más natural. Esto se hace tirando un número al azar (en realidad un número pseudo-aleatorio) y sumando ese valor a la dirección:

class Organismo{ … void variarAngulo( float amplitud ){ //varia la dirección con una amplitud determinada float radi = radians( amplitud ); //transforma los grados en radianes direccion += random( -radi , radi ); //aplica un valor al azar en el rango } ... void mover(){ //actualiza la ubicación del organismo variarAngulo( 30 ); //varia la direccion en un rango de 30 grados en cada lado dx = velocidad * cos(direccion); //deduce el desplazamiento en X dy = velocidad * sin(direccion); //deduce el desplazamiento en Y x += dx; //aplica los desplazamiento y += dy; //aplica los desplazamiento } ... Es importante destacar que los ángulos en informática se miden en radianes (ver en Wikipedia), sin embargo a nosostros nos resulta más sencillo manejarnos con grados. Debido a eso la función void variarAngulo( float amplitud) recibe el parámetro amplitud expresado en grados, pero interamente convierte este ángulo en radianes con la función de Processing radians( ). Para aplicar la función variarAngulo se la invoca desde dentro de la acción mover, pasándole como parámetro el valor 30 (en este caso), lo que significa que la dirección puede variar hasta 30 grados en ambos sentidos (horario y antihorario).

Espacio toroidal

En este ejemplo se implementó un espacio toroidal, esto es un espacio continuo en el sentido vertical y horizontal. este término viene de la curva llamada toroide . Sencillamente, esto significa que cuando algo sale de la escena por el borde derecho, entonces reaparece por el izquierdo, lo mismo sucede en sentido contrario, es decir que cuando sale por la izquierda vuele a ingresar por la derecha, y así con los bordes superior e inferior. De esta forma la escena no tiene límite (o mejor dicho sus límites se tocan), como cuando un circunda una esfera (como nuestro planeta).

La forma en que se realiza esto revisando si los organismos se salen de los bordes, y entonces haciéndolos reingresar por el borde opuesto:

class Organismo{ … void mover(){ //actualiza la ubicación del organismo variarAngulo( 30 ); //caria la direccion en un rango // de 30 grados para cada lado dx = velocidad * cos(direccion); //deduce el desplazamiento en X dy = velocidad * sin(direccion); //deduce el desplazamiento en Y x += dx; //aplica los desplazamiento y += dy; //aplica los desplazamiento if( toroidal ){ //si el espacio es toroidal // entonces revisa si se pasó de límite x = ( x>width ? x-width : x ); x = ( x<0 ? x+width : x ); y = ( y>height ? y-height : y ); y = ( y<0 ? y+height : y ); } } ...

En el código anterior se utiliza una variable booleana para poder configurar el espacio como toroidal o no.

Territorio

Se aprende a administrar un territorio para resolver el problema de la explosión combinatoria originada en la gran cantidad de interacciones entre individuos. En el apartado anterior vimos como crear organismos virtuales autónomos. También vimos como lograr que estos organismos se muevan de forma natural, pero hasta el momento, estos organismos no logran percibir a sus pares. Es decir, a pesar de moverse por un espacio en común, no pueden verse entre sí. Es ahora, entonces, cuando veremos como lograr que estos seres virtuales interactuen entre sí, para ello es preciso que puedan percibirse unos a los otros. En este caso en particular, la forma en que estos agentes se manifiestan es a partir de su posición y movimiento, dado que eso es todo lo que por el momento saben hacer. Por lo tanto la forma en que ellos pueden percibir al resto es conociendo la posición de los demás.

La explosión combinatoria La forma concreta en la que se logra que los agentes interactúen, es haciendo que cada uno de ellos compare su posición en el espacio, con la de todos los demás. Por ejemplo, si tenemos 5 agentes, el 1 deberá comparar su posición en el 2, el 3, el 4 y el 5.

Pero luego cada uno de los otras deberá hacer lo mismo:

Si el vínculo que se establece entre dos agentes no tiene dirección, es decir que, visto desde los dos individuos es lo mismo, entonces, cuando hay 4 agentes el número total de vínculos simples es 6. Pero, si en cambio, el vínculo cambia según la dirección, entonces con 4 agentes tenemos una 12 vínculos En la tabla anterior se puede ver claramente que pequeñas cantidades de agentes implican grandes cantidades de vínculos. El problema de esto es que este tipo de simulaciones son interesantes cuando la cantidad de agentes superan los cientos. Sin embargo la cantidad de interacciones que requieren cientos de agentes puede ser muy alta para seguir sosteniendo la performance de una aplicación en tiemporeal.

División del territorio Supongamos que tenemos un escenario con 20 agentes y que queremos que estos se muevan libremente, pero cuando se cruzan con otro, lo esquiven, haciendo que salgan en la dirección opuesta. Un escenario como este necesitaría de 380 interacciones. Pero si se analiza el problema en profundidad, se observa que no tiene sentido comparar los agentes que se encuentran lejos unos de otros. El problema de este razonamiento, es que para saber cuales están lejos de los otros, es necesario hacer la comparación, después de todo, el sentido de la comparación era revisar las distancias. Existe otra forma de resolver el problema. Esta consiste en dividir el espacio en espacios más pequeños, en los que quepan menos agentes. De esta forma, el espacio queda dividido en un número homogéneo de celdas, las cuales albergan menor cantidad de agentes. Así se puede determinar que agentes se encuentran cerca unos de otros, dado que en principio pertenecerán a la misma celda. Por ejemplo, en el diagrama que se encuentra arriba, se pueden ver 20 agentes distribuidos en 9 celdas. Algunas celdas poseen desde 1 hasta 5 agentes, algunas no poseen ninguno. Con esta división la cantidad de interacciones se reduce a 52. Cuanto más chicas sean las celdas, menor será la cantidad de agentes que quepan dentro de cada una de estas, y por ende menor la cantidad de interacciones. Sin embargo, en casos extremos el tamaño de las celdas podría ser tan pequeño que sólo entrase un o ningun agente, por lo que estos serían incapaces de ver al resto. En el ejemplo anterior también se puede ver que el agente 20 y el 10 (aproximadamente en el centro de la escena) se encuentra relativamente cerca, pero no interactuarán, dado que se encuentran en diferentes celdas, y sin embargo la 20 interactúa con la 3, siendo que se encuentra más lejana que la 10.

Esquivando a los otros En el ejemplo que esta arriba, los organismos son capaces de recorrer el espacio e intentear esquivar a los otros organismos. Para esto fue necesario implementar un algoritmo que administre el territorio de la forma antes descripta. La idea del mismo, es que en cada ciclo (cada fotograma) de la ejecución, luego de mover los organismos, se los ubica a cada uno en la celda de territorio que les corresponde. Para esto se crearon un conjunto de funciones que organizan el desarrollo de este algoritmo: void draw(){ … revisar_Territorio(); resolver_Encuentros_Organismos(); mover_Organismos(); dibujar_Organismos(); ... En el código escrito arriba se puede observar el ciclo de funcionamiento:

1- Revisa la posición de cada organismo y los ubica en la celda de territorio que les corresponde.

2- Recorre celda por celda el territorio y en cada una de estas revisa los encuentros entre organismos.

3- Mueve los organismos en función de lo resuelto en cada encuentro.

4- Dibuja los organismos en pantalla. Los dos últimos pasos son exactamente iguales a los vistos en el apartado anterior. De hecho el comportamiento mover() (de la clase Organismo) no ha cambiado en nada. void mover_Organismos(){ for(int i=0;i
} void dibujar_Organismos(){ for(int i=0;i
Administrando el territorio Si bien la clase Organismo tiene un comportamiento para resolver el encuentro con otro. Es necesario ejecutar esta acción desde fuera, presentándole los diferentes otros a cada organismo. Como vimos al principio hacer esto entre todos los organismos en forma indiscriminada puede generar problemas por la explosión combinatoria. Por eso es necesario administrar el territorio según el criterio que antes describimos. Para ello desarrollamos una clase Territorio que a su vez está conformada por una matriz de objetos de tipo T_Lugar. La función de los objetos T_Lugar es registrar los organismos de cada celda en que se divide el territorio. Para ello posee tres arreglos, dos para las posiciones de los organismos (x[ ] e y[ ] ) y otro para registrar los identificadores de estos ( id[ ] el número de índice en el arreglo de organismos). class T_Lugar{ int cantidad; int limite; int fila,col; float x[], y[]; int id[]; T_Lugar( int col_ , int fila_ ){ fila = fila_; col = col_; cantidad = 0; limite = 100; x = new float[limite]; y = new float[limite]; id = new int[limite]; } void agregar( float x_ , float y_ , int id_ ){ if( cantidad < limite-1 ){ x[ cantidad ] = x_; y[ cantidad ] = y_; id[ cantidad ] = id_; cantidad ++;

} } } Las variables fila y col sirven para almacenar la posición de la celda en el territorio. Sólo es útil para hacerle posteriores consultas a la celda. El comportamiento agregar( float x_ , float y_ , int id_ ), que es el que nos interesa, permite agregar un nuevo organismo a esta celda. Por cada organismo que se agrega, se carga en los arreglos (x[ ], y[ ] e id[ ] ) y luego se incrementa la variable cantidad. A su vez el objeto territorio se encarga de verificar en cuál celda está el organismo: class Territorio{ float ancho; float alto; int filas; int col; int modH,modV; T_Lugar lugares[][]; Territorio( float anchoPantalla , float altoPantalla , int filas_ , int col_ ){ //inicializa el objeto definiendo la matriz de celdas ancho = anchoPantalla; alto = altoPantalla; filas = filas_; col = col_; modH = int(ancho/col); modV = int(alto/filas); lugares = new T_Lugar[ col ][ filas ]; for(int i=0;i0 && x0 && y= col ? col-1 : cualX); cualY = (cualY >= filas ? filas-1 : cualY); //agrega el objeto en la celda elegida lugares[ cualX ][ cualY ].agregar( x , y , id ); } } ...

Como se observa arriba, el constructor de la clase Territorio recibe como parámetros las dimensiones de la pantalla (la escena) y la cantidad de filas y columnas de la matriz de celdas en las que se divide el territorio. En función de estos parámetros, el constructor calcula las variables modH y modV, las cuales describen las dimensiones (en píxels) de cada celda. El comportamiento ubicar( float x , float y , int id ) se encarga de recibir la posición e identificación de cada organismo, para calcular en cúal celda está ubicado, esto lo hace con las operaciones: int cualX = int(x/modH) int cualY = int(y/modV)

Luego le envía la información a la celda seleccionada para que esta lo agregue a su lista: lugares[ cualX ][ cualY ].agregar( x , y , id ). Las funciones que comandan estas acciones desde la estructura principal son revisar_Territorio( ) y resolver_Encuentros_Organismos( ). La primera es bastante sencilla: void revisar_Territorio(){ miTerritorio = new Territorio( width , height , celdas , celdas); for(int i=0;i
El cuerto ciclo, el correspondiente a la variable l, se encarga de seleccionar un nuevo organismos para enfrentar al ya seleccionado, por eso el recorrido del ciclo se hace desde l=k+1. La combinación de recorrido de los dos ciclos for (el de k y l) asegura que se recorre todos los casos de combinación (en tanto no se llegue al límite de encuentros), sin nunca llegar hacer que k y l sean iguales: for( int k=0 ; k < esteLugar.cantidad-1 && k
Especies y conductas En este capítulo se explica como crear diferentes especies, las cuales sean capaces de registrar al otro y en función de eso tomar decisiones. También se ve un modelo práctico de como establecer relaciones entre distintas especies.

Ecosistema En los apartados anteriores todos los organismos se comportaban igual y eran del mismo tipo, en cambio en el ejemplo de arriba los organismos son de diferentes especies, profundisaremos en esto más adelante. Otro de los avances que aparece en este ejemplo es la implementación de un objeto de tipo Ecosistema que permite regular la actividad de este ambiente virtual:

class Ecosistema{ Organismo[] animales; int cantOrganismos; Territorio miTerritorio; int celdas; float probabilidadDepredadores = 5; float probabilidadHerviboros = 35; Ecosistema( int cantOrganismos_ , int celdas_ ){ ... void iniciar_Organismos(){ ... void revisar_Territorio(){ ... void resolver_Encuentros_Organismos(){ ... void accionar_Organismos(){ ... void dibujar_Organismos(){ ... }

Esta clase Ecosistema reune las funciones de los apartados anteriores y algunas nuevas, pero en definitiva, su función es accionar los comportamientos de los organismos. Con respecto a las funciones anteriores, hubo que cambiar el comportamiento mover_Organismos( ) por uno nuevo, de caracter más general, llamado accionar_Organismos( ).

Especies Volviendo al tema de las especies, si bien existen muchas formas posibles de resolver este tema (al igual que con las problemáticas anteriores), decidimos que la más sencilla es que la especie sea un atributo del organismo. De forma tal que desde el programa principal se trate a todos los organismos por igual, sin distinguir especies: Ecosistema bio; //declara un objeto ecosistema void setup(){ bio = new Ecosistema( 40 , 4 ); //define un ecosistema // de 40 organismos con una division del // territorio de 4x4 celdas ... } void draw(){ ... bio.revisar_Territorio(); bio.resolver_Encuentros_Organismos(); bio.accionar_Organismos(); bio.dibujar_Organismos(); ...

En el algoritmo de arriba no se puede distinguir a los organismos de diferentes especies. Esto nos facilita el hecho de que: 1) los mismos comportamientos son aplicables a todo 2) no es necesario saber de que manera se va a distribuir la memoria entre las poblaciones, dado que esto puede ir cambiando dinamicamente. Para realizar dicha implementación fue necesario realizar algunos cambios en la clase Organismo: class Organismo{ int especie; //define la especie //define estas variables que serán // usadas como constantes de especie int PLANTA = 0; int HERVIBORO = 1; int DEPREDADOR = 2; ... void asignarEspecieAlAzar( float probDepredador , float probHerviboro ){ //le asigna una especie al azar ... } void defineCaracteristicas(){ //define la velocidad // de movimiento en función de la especie ... } void dibujar(){ //dibuja el organismo … if( especie == PLANTA ){ //si es una planta lo // dibuja como a un circulos … }else if( especie == HERVIBORO ){ //si es herviboro //lo dibuja como a un triangulo … }else{ //si es depredador lo dibuja como // a un rectángulo … } } void accionar(){ ... }

Primero que nada, se creó una variable especie encargada de registrar dicho dato. A partir del valor de esta variable, ciertos comportamientos realizan diferentes operaciones para cada especie. Un ejemplo de esto es el comportamiento dibujar( ), que representa a cada especie de diferentes formas.

También se agregó un comportamiento accionar( ) que reemplaza a mover( ), dado que las acciones implican más que el sólo moverse. A partir de ahora, mover( ) no es más el comportamiento principal, sino que será llamado esporádicamente por el comportamiento accionar( ). Existen tres variables, que en realidad representan valores que desde nuestros algoritmo son considerados constantes. Estas variables representan a las diferentes especies posibles: PLANTA, HERVIBORO y DEPREDADOR. Otro cambio importante es la función defineCaracteristicas( ), que se encarga de asignar diferentes comportamientos y representaciones a cada especie: class Organismo{ … void defineCaracteristicas(){ //define la velocidad de movimiento en función de la especie float tinta; if( especie == DEPREDADOR ){ velocidad = 8; tinta = random( 0,30 ); } else if( especie == HERVIBORO ){ velocidad = 4; tinta = random( 150,250 ); } else{ velocidad = 0; tinta = random( 80,150 ); } borde = color(tinta,150,150,200); relleno = color(tinta,300,300,100); } … En este caso, la diferencia de comportamiento esta dado por las diferentes velocidades que se le asigna a cada especie, pero en ejemplos anteriores esta función será más compleja.

Conductas y relaciones En el ejemplo anterior (Vida_06) la diferencia de especie sólo implicaba una cambio en la representación y en la velocidad de avance. Pero en el ejemplo de arriba (Vida_07) cada individuo, de cada especie, responde de diferentes maneras frente al encuentro con otros individuos de igual o diferente especie. Según la especie de dos individuos y la distancia entre ellos, pueden surgir una gran diversidad de acciones. Para facilitar las cosas, se estipuló un conjunto de acciones posibles entre individuos. Estas acciones forman parte de la información agregada al objeto Organismo:

class Organismo{ ... int conducta; //registra la conducta a llevar a cabo Organismo elOtro; //registra el organismo objeto de la conducta //define estas variables que seran usadas //como constantes de conductas //también sirven para declarar las prioridades int INDIFERENCIA = 0; int ALEJARSE = 1; int PERSEGUIR = 2; int ESQUIVAR = 3; int REPRODUCIR = 4; int HUIR = 5; int COMER = 6; ...

Estas 7 conductas posibles van desde la INDIFERENCIA hasta COMER y el orden en que están representa el nivel de prioridad, siendo COMER la conducta prioritaria e INDIFERENCIA la de menor prioridad. Esto quiere decir que frente a varias conductas posibles (generadas a partir del encuentro con varios individuos) prevalecen las conductas de mayor prioridad. Un tema fundamental de esto, es el cómo se resuelven la conductas en los encuentros: class Organismo{ … void resolverEncuentro( Organismo otro ){ float distancia = dist( x , y , otro.x , otro.y ); int actitudConEste; if( distancia < radio*2 ){ //si lo está tocando actitudConEste = conductaColision[ especie ][ otro.especie ]; } else if( distancia < radio*6 ){ //si está a un // cuerpo de distancia actitudConEste = conductaCerca[ especie ][ otro.especie ]; } else{ actitudConEste = conductaLejos[ especie ][ otro.especie ]; } if( actitudConEste > conducta ){ conducta = actitudConEste; elOtro = otro; } } ...

En comportamiento resolverEncuentro( ) , lo primero que hace es calcular la distancia del otro individuo, esto es por que la conducta a tomar depende mucho de la distancia entre dos organismos. Por ejemplo, un herviboro se encuentra medianamente cerca de una planta, seguramente decidirá acercarse para comerla, pero si en ese momento tiene más cerca a un depredador que está a punto de comerlo, entonces la conducta cambiará radicalmente hacia la huida. Una vez que calcula la distancia, carga la actitud que desea tomar con el individuo en cuestion ( actitudConEste ), tomando dicha dato de una matriz de conductas, usando como criterio de selección, la distancia y las especies de cada uno de estos dos individuos. En realidad existen tres matrices, una para cada una de las distancias evaluadas: 1) conductaColision[ ][ ] esta matriz establece la relación entre los individuos cuando chocan entre sí. 2) conductaCerca[ ][ ] esta matriz es para cuando están cerca pero no colisionan. 3) conductaLejos[ ][ ] esta matriz es para cuando están un poco más lejos. Cada una de estas matrices es cargada con la conducta que corresponde: class Organismo{ … void iniciarMatricesDeConducta(){ … // conducta para cuando dos organismos COLISIONAN conductaColision[ PLANTA ][ PLANTA ] = INDIFERENCIA; conductaColision[ PLANTA ][ HERVIBORO ] = INDIFERENCIA; conductaColision[ PLANTA ][ DEPREDADOR ] = INDIFERENCIA; conductaColision[ HERVIBORO ][ PLANTA ] = COMER; conductaColision[ HERVIBORO ][ HERVIBORO ] = ESQUIVAR; conductaColision[ HERVIBORO ][ DEPREDADOR ] = HUIR; conductaColision[ DEPREDADOR ][ PLANTA ] = ESQUIVAR; conductaColision[ DEPREDADOR ][ HERVIBORO ] = COMER; conductaColision[ DEPREDADOR ][ DEPREDADOR ] = ESQUIVAR; // conducta para cuando dos organismos ESTAN CERCA conductaCerca[ PLANTA ][ PLANTA ] = INDIFERENCIA; conductaCerca[ PLANTA ][ HERVIBORO ] = INDIFERENCIA; conductaCerca[ PLANTA ][ DEPREDADOR ] = INDIFERENCIA;

conductaCerca[ HERVIBORO ][ PLANTA ] = PERSEGUIR; conductaCerca[ HERVIBORO ][ HERVIBORO ] = INDIFERENCIA; conductaCerca[ HERVIBORO ][ DEPREDADOR ] = ALEJARSE; conductaCerca[ DEPREDADOR ][ PLANTA ] = INDIFERENCIA; conductaCerca[ DEPREDADOR ][ HERVIBORO ] = PERSEGUIR; conductaCerca[ DEPREDADOR ][ DEPREDADOR ] = INDIFERENCIA; // conducta para cuando dos organismos ESTAN LEJOS conductaLejos[ PLANTA ][ PLANTA ] = INDIFERENCIA; conductaLejos[ PLANTA ][ HERVIBORO ] = INDIFERENCIA; conductaLejos[ PLANTA ][ DEPREDADOR ] = INDIFERENCIA; conductaLejos[ HERVIBORO ][ PLANTA ] = INDIFERENCIA; conductaLejos[ HERVIBORO ][ HERVIBORO ] = INDIFERENCIA; conductaLejos[ HERVIBORO ][ DEPREDADOR ] = ALEJARSE; conductaLejos[ DEPREDADOR ][ PLANTA ] = INDIFERENCIA; conductaLejos[ DEPREDADOR ][ HERVIBORO ] = PERSEGUIR; conductaLejos[ DEPREDADOR ][ DEPREDADOR ] = INDIFERENCIA; } ...

De esta forma, estas matrices describen en función del organismo observador y del observado, cuál es la actitud propuesta. Por ejemplo, la siguiente carga de la matriz: conductaCerca[ DEPREDADOR ][ HERVIBORO ] = PERSEGUIR; significa que cuando un DEPREDADOR está cerca de un HERVÍBORO, entonces decide PERSEGUIR. Si se observa detenidamente, se puede ver que las PLANTAS son INDIFERENTES a todo otro organismo, sin importar la especie, ni cercanía. En cambio, un HERVIBORO, prefiere COMER al colisionar una PLANTA, ESQUIVAR si choca con otro de su especie, y HUIR si es alcanzado por un DEPREDADOR. Por último, un DEPREDADOR prefiere ESQUIVAR una PLANTA, COMER a un HERVIBORO y ESQUIVAR a otro de su especie. Por último se ace necesario implementar estas conductas: class Organismo{ … void accionar(){ if( conducta == INDIFERENCIA ){ mover(); } else if( conducta == HUIR ){ huirDelOtro( elOtro ); } else if( conducta == ESQUIVAR ){ huirDelOtro( elOtro ); } else if( conducta == COMER ){ comerAlOtro( elOtro ); } else if( conducta == REPRODUCIR ){ //SIN IMPLEMENTAR mover(); } else if( conducta == ALEJARSE ){ huirDelOtro( elOtro ); } else if( conducta == PERSEGUIR ){ perseguirAlOtro( elOtro ); } conducta = INDIFERENCIA; } ...

En cada ciclo de ejecución, el comportamiento accionar( ) se encarga de ejecutar la acción que corresponda a cada conducta. Existen conductas que se pueden realizar con el organismo sólo (es decir que no requiere de la participación de otro), como mover( ), pero existen otras acciones que requieren de un otro, como objeto de la conducta, por ejemplo comer(). Para esos casos, existe la variable elOtro, que indica cuál es el objeto con el que se relaciona la acción.

Organismos que comen otros Una de las relaciones posibles entre dos organismos, es que uno se coma al otro. Para lograr que un organismo pueda comer a otro, es necesario que los organismos puedan morir. Por eso, se agregó en el objeto Organismo, una variable llamada estaVivo. Esta variable que se inicia con el valor verdadero (true), puede ser puesta en false y por ende hacer que el organismo deje de interactuar, es decir, deje de estar vivo. De esta forma, fue necesario agregar en el organismo un comportamiento llamado morir( ), cuya única finalidad es lograr que un organismo le pueda ordenar a otro, morirse, como resultado de habérselo comido. Tambien se creó una función vive( ), que sirve para consultar cuándo el organismo está vivo. class Organismo{ … void morir(){ estaVivo = false; } boolean vive(){ return estaVivo; } ...

Una vez que fueron creadas esta variables, entonces es hora de que un organismo se coma a otro: class Organismo{ … void comerAlOtro( Organismo otro ){ otro.morir(); } ...

Pero como nuestros organismos ahora son capaces de morir, entonces se hace necesario discriminar los vivos de los muertos, para no dibujar, mover, ni accionar a organismos que ya no existen: class Ecosistema{ … void accionar_Organismos(){ for(int i=0;i
En este comportamiento se puede ver que, si bien el ciclo for recorre todo el arreglo de organismos, sólo se accionan a aquellos que cumplen con la condición de estar vivo: if( animales[ i ].vive() ) Esta politica se adoptó con las siguientes funciones del ecosistema: accionar_Organismos( ) dibujar_Organismos( ) revisar_Territorio( )

Organismos que se reproducen Ahora, es el momento de afrontar un problema, que es el de lograr que estos organismos puedan reproducirse. Las dificultades asociadas con este problema, están en que los organismos, forman parte de un arreglo, pero ellos no pueden administrar esta estructura de datos, dado que la misma es externa a ellos. Esta estructura pertenece al objeto Ecosistema. Para decirlo de otra manera, los objetos Organismo, pertenecen al arreglo, que, a su vez, pertenecen al objeto Ecosistema. Por esto, el objeto que se puede encargar de administrar este arreglo, es el objeto Ecosistema. Debido a esto, cuando un organismo desea reproducirse, le tiene que pedir al ecosistema, que genere un nuevo organismo copia del primero, por lo que es necesario crear una vía de comunicación entre estos dos objetos. class Organismo{ Ecosistema miEcosistema; ... void asociarEcosistema( Ecosistema unEcosistema ){ miEcosistema = unEcosistema; } ... }

Como se ve arriba, dentro del objeto Organismo se creó una variable llamada miEcosistema, la cual es de tipo Ecosistema. El sentido de esta variable, no es generar un ecosistema dentro de cada organismo, sino, crear una variable que apunte al ecosistema, para que el organismo se pueda comunicar con este. Por eso, no existe ningún comando que ejecute un new, es decir que genere un nuevo objeto, ejecutando un constructor. En su lugar, se ejecuta el comportamiento void asociarEcosistema( Ecosistema unEcosistema ), en donde se le pasa como parámetro el ecosistema al que quedará asociado el organismo. Por otro lado, es necesario crear, en el objeto Ecosistema, un comportamiento que se encargue de hacer nacer nuevos individuos: class Ecosistema{ ... void nacer_Organismo( int cualEspecie , float x , float y){ for(int i=0;i
Obviamente es necesario crear nuevos comportamientos en el objeto Organismo. Un comportamiento void nacer( int especie_ , float x_ , float y_ ), que es el que recibe la información del nuevo organimos, en este caso, posición y especie. Otro comportamiento necesario es void reproducirse( Organismo otro ) que se encarga de pedir al Ecosistema un nuevo nacimiento:

class Organismo{ … void nacer( int especie_ , float x_ , float y_ ){ //nace el organismo iniciar( x_ , y_ ); //lo inicializa especie = especie_; //le asigna la especie defineCaracteristicas(); //y define sus caracteres } … void reproducirse( Organismo otro ){ miEcosistema.nacer_Organismo( especie , x+random(-radio,radio) , y+random(radio,radio) ); huirDelOtro( otro ); } ... }

Energia Se recorre un modelo sencillo (e introductorio) de como simular el comportamiento de los seres vivos con relación a la energía. Hasta este punto hemos logrado hacer organismos virtuales capaces de moverse, de pertenecer a diferentes especies y de actuar en consecuencia, comiendo, matando, reproduciendose y muriendo, pero todo esto se ha realizado sin ningún costo energético. Por lo que estos organismos son eternos, ha menos que alguien los mate. Por ejemplo: cada ser de este ecosistema puede varagr indefinidamente y jamás morir de hambre. La energía es uno de los factores principales de la vida, dado que los organismos se conducen en pos de conseguirla y es por ello que comen.

La falta de energía y la muerte Para poder representar este característica de la vida, haremos nuevas modificaciones en nuestro objeto Organismo. En este caso, tenemos que agregar un conjunto de variables relacionadas con el estado energético del organismo, así como los valores de consume y recuperación de energía. class Organismo{ ... float energia; float energiaLimite; float gastoMoverse; // el gasto de energia cuando se mueve float gastoReproducirse; // el gasto de energia // cuando se reproduce float gananciaComer; //lo que recupera de energia cuando come ... La variable energia es la encargada de registrar el estado energético del organismo. Las variables gastoMoverse y gastoReproducirse sirven para especificar cuánta energía se consume en dichas acciones. Y por último, la variable gananciaComer, sirve para especificar cuánta energía se recupera al momento de comer. La pérdida total de energía significa la muerte del organismo, por eso hubo que implementar dicha muerte en el comportamiento accionar(): class Organismo{ ... void accionar(){ … if( energia<0 ){ morir(); if( especie == DEPREDADOR ){ for(int j=0;j<5;j++){ miEcosistema.nacer_Organismo( PLANTA ,

x+random(- radio*2,radio*2) , y+random (-radio*2,radio*2) ); } } } } ... Como se puede observar arriba, en el comportamiento accionar( ), aparece un condicional en donde se evalúa si la cantidad de energía es menor que cero: if( energia<0 ). Si esta condición es verdadera, entonces se ejecuta el comportamiento morir( ). Debajo de esto existe otra condición que se evalúa, en la que se pregunta si el ser que está muriendo es un depredador, en cuyo caso se pide al ecosistema que haga nacer 5 plantas en el lugar de la muerte. El sentido de esta acción es para reestablecer el equilibrio en el ecosistema, dado que hasta el momento, a los depredadores no los puede comer nadie, y las plantas no tienen forma de reproducirse, por lo que una solución sencilla es hacer que cuando los depredadores mueren de inanición (que es la única forma de morir que tienen) puedan aparecer plantas en su lugar, una forma metafórica que imita la sucución de hechos en los cuales un depredador (sin enemigos naturales) muere, su cuerpo se disuelve y alimenta a la tierra que da energía a nuevas plantas, es decir se cierra el ciclo en donde: los hervíboros comen plantas, los depredadores comen a los hervíboros y por último, las plantas se alimentan de los depredadores.

El consumo de energía Dado que la energía determina en cierta medida la posibilidad de vida de estos organismos, un punto crucial de su administración son los consumos de energía que implican las accioones. Los seres vivos requieren energía para sostener su propia estructura, en pos de conseguirla aplican diferentes estrategías, por ejemplo, los animales salen a comer a otros seres, pero las acciones que se realizan implican en sí mismas un consumo de esa energía. En nuestro organismo se han modificado ciertas accionas con el fin de integrar el consumo de energía en su desarrollo: class Organismo{ ... void mover(){ //actualiza la ubicación del organismo … energia -= gastoMoverse; } ... void reproducirse( Organismo otro ){ if( energia > 50 && edad>60 && otro.edad>60 ){ miEcosistema.nacer_Organismo( especie , x+random(-radio,radio) , y+random(-radio,radio) ); energia -= gastoReproducirse; } ... } ... Por ejemplo, el comportamiento mover( ) descuenta, de la energía del organismo, el gasto de moverse. El comportamiento reproducirse( ) tambien descuenta de la energía el gasto en cuestión. Si nos fijamos detenidamente en este último comportamiento, notaremos que no sólo consume energía, sino que la propia posibilidad de realizarse está determinada por la condición de poseer un mínimo de energía. Es decir, si el organismo no posee un mínimo de 50, como cantidad de energía, entonces no puede reproducirse: if( energia > 50 && edad>60 && otro.edad>60 ), tambien consulta por la edad del organismo, algo así colmo "exigirle una mínima madurez para poder repruducirse".

La recuperación de energía La energía no sólo se consume, también es posible recuperarla: class Organismo{ ... void comerAlOtro( Organismo otro ){ if( energia < 70 ){

energia += gananciaComer; otro.morir(); } else{ huirDelOtro( otro ); } }

En este caso, cuando el organismo como a otro organismo, entonces recupera energía, en proporción con una variable llamada gananciaComer. Aquí, nuevamente las acciones se ven restringidas a la cantidad de energía disponible, esto sucede en el condicional que pregunta si la energía es menor a 70: if( energia < 70 ) Esto es por lograr que los organismos que "ya estan satisfechos", es decir que no requieren energía por el momento, no coman desforadamente. Con esta condición se logra que los organismos sólo coman cuando les falta energía: es decir, cuando tengan hambre.

Related Documents

Vida Artificial
May 2020 5
Artificial Insemination
December 2019 13
Sangre Artificial
April 2020 7
Cutremur Artificial
May 2020 8
Artificial Blood
June 2020 10

More Documents from ""