Ecosistema
Ejemplo Vida 06 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(){ ... }Hecho en Processing
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(); ... }Hecho en Processing
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(){ ... } }Hecho en Processing
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); } }Hecho en Processing
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
Ejemplo Vida 07 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; ... }Hecho en Processing
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; } } ... }Hecho en Processing
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; } ... }Hecho en Processing
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; } ... }Hecho en Processing
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; } ... }Hecho en Processing
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(); } ... }Hecho en Processing
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<cantOrganismos;i++){ //se recorre cada animal y : if( animales[i].vive() ){ //solo si esta vivo animales[i].accionar(); //cada uno amina hacia la comida //y actualiza su energia } } } ... }Hecho en Processing
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
Ejemplo Vida 08 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; } ... }Hecho en Processing
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<cantOrganismos;i++){ //se recorre cada animal y : if( ! animales[i].vive() ){ //solo si esta muerto lo // usa para nacer uno nuevo animales[i].nacer( cualEspecie , x , y ); //dibuja cada animal break; } } } ... }Hecho en Processing
Este comportamiento llamado void nacer_Organismo( int cualEspecie , float x , float y) se encarga de buscar en el arreglo aquellos casilleros que este ocupados por un organismo que este vido. Esto lo hace recorriendo uno a uno los lugares de arreglo y preguntando a cada organismo si está vivo:
for(int i=0;i<cantOrganismos;i++){
if( ! animales[i].vive() ){
Cuando encuentra un organismo muerto, entonces le pide que vuelva a nacer "con una nueva identidad": animales[ i ].nacer( cualEspecie , x , y )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 ); } ... }Hecho en Processing