Professional Documents
Culture Documents
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.
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.):
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.
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:
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
}
...
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
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
En el código anterior se utiliza una variable booleana para poder configurar el espacio como toroidal o no.
Territorio
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.
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
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.
void draw(){
…
revisar_Territorio();
resolver_Encuentros_Organismos();
mover_Organismos();
dibujar_Organismos();
...
void mover_Organismos(){
for(int i=0;i<cantAnimales;i++){ //se recorre cada animal y :
animales[i].mover(); //cada uno amina hacia la
// comida y actualiza su energia
}
}
void dibujar_Organismos(){
for(int i=0;i<cantAnimales;i++){ //se recorre cada animal y :
animales[i].dibujar(); //dibuja cada animal
}
}
Como se ve arriba, estas nuevas funciones lo único que hacen es recorrer el arreglo de organismos y ejecutar sus
comportamientos mover( ) y dibujar( ).
La duda que puede surgir en este punto es ¿cómo es que sin cambios en el comportamiento mover( ) los organismos
se comportan distintos? Esto se logra por que la desición de hacia dónde moverse se resuelve en un omportamiento,
llamado resolverEncuentro( ):
El comportamiento resolverEncuentro( ) recibe un objeto Organismo como parámetro (llamado otro). Dado que otro
es también un organismo, posee los mismos datos que este objeto, es decir: x, y, dirección, velocidad, dx, dy,
radio,etc. Entonces utiliza los datos de posición (x e y) para compararlos con los propios y así estimar la distancia del
otro: dist( x , y , otro.x , otro.y ) < radio*4 (la función dist(x1,y1,x2,y2) calcula la distancia entre dos puntos ). Si esta
condición se cumple, entonces toma la dirección contraria: direccion = atan2( y-otro.y , x-otro.x ) ( la operación
atan2(y2-y1,x2-x1) devuelve el ángulo descripto por dos puntos (la pendiente).
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;i<col;i++){
for(int j=0;j<filas;j++){
lugares[i][j] = new T_Lugar(i,j);
}
}
}
void ubicar( float x , float y , int id ){
//este comportamiento ubica a cada objeto en su celda
if( x>0 && x<ancho && y>0 && y<alto){
int cualX = int(x/modH);
//define el lugar horizontal en el que //cae el objeto
int cualY = int(y/modV);
//define el lugar vertical en el que cae //el objeto
cualX = (cualX >= 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<cantAnimales;i++){ //se recorre cada animal y :
miTerritorio.ubicar( animales[i].x , animales[i].y , i );
}
}
void resolver_Encuentros_Organismos(){
int limiteEncuentros = 20;
for( int i=0 ; i<miTerritorio.col ; i++ ){
//recorre una por una las
for( int j=0 ; j<miTerritorio.filas ; j++ ){
//celdas del territorio
T_Lugar esteLugar = miTerritorio.lugar( i , j );
//toma cada lugar del territorio
for( int k=0 ; k < esteLugar.cantidad-1 && k<limiteEncuentros ; k++ ){
// toma uno por uno los objetos de este lugar
int id1 = esteLugar.id[k];
// recupera el id
for( int l=k+1 ;
l < esteLugar.cantidad && l<limiteEncuentros ; l++ ){
// toma otro objeto del lugar
int id2 = esteLugar.id[l];
// recupera el id
animales[id1].resolverEncuentro( animales[id2] );
// enfrenta al organismo
// 1 con el 2
animales[id2].resolverEncuentro( animales[id1] );
// enfrenta al organismo
// 2 con el 1
}
}
}
}
}
Los dos primeros ciclos for (los que corresponden a las variable i y j) se encargan de recorrer el territorio celda por
celda. La instrucción T_Lugar esteLugar = miTerritorio.lugar( i , j ) carga en la variable esteLugar la celda
correspondiente a la posición( i y j). Luego, el ciclo for correspondiente a la variable k se encarga de recorrer uno a uno
los organismos de esa celda.
En este ciclo, la condición k<limiteEncuentros sirve para que la cantidad de encuentros no superen un límite
preestablecido. Esto es dado que podría suceder el hipotético caso de que todos los organismos estén en una única
celda de todo el territorio, en cuyo caso estaríamos con el mismo problema que al principio. Frente a esta posible
situación, no nos queda más remedio que limitar la cantidad de encuentros.
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<limiteEncuentros ; k++ ){
for( int l=k+1 ; l < esteLugar.cantidad && l<limiteEncuentros ; l++ ){
Por último, se les pide a los organismos seleccionados que enfrenten a su pareja:
animales[ id1 ].resolverEncuentro( animales[ id2 ] )
animales[ id2 ].resolverEncuentro( animales[ id1 ] )
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;
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:
En el algoritmo de arriba no se puede distinguir a los organismos de diferentes especies. Esto nos facilita el hecho de
que:
Para realizar dicha implementación fue necesario realizar algunos cambios en la clase Organismo:
class Organismo{
int especie; //define la especie
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
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.
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.
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.
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<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
}
}
}
...
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() )
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<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;
}
}
}
...
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 );
}
...
}
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.
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.