You are on page 1of 19

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<cantAnimales;i++){ //se recorre cada animal y
animales[i].dibujar(); //dibuja cada animal
}
...
}
...
void iniciar(){
animales = new Organismo[cantAnimales]; //Se inicia el arreglo
for(int i=0;i<cantAnimales;i++){ //se recorre cada animal
animales[i] = new Organismo(); // se inicia cada animal
}
}

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 tiempo-
real.
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<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( ):

void resolverEncuentro( Organismo otro ){


if( dist( x , y , otro.x , otro.y ) < radio*4 ){
//si se encuentra a menos de dos cuerpos de distancia, sale en la dirección opuesta
direccion = atan2( y-otro.y , x-otro.x );
}
}

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 );
}
}

Inicializa el territorio, es decir, vacía todas las celdas ejecutando el contructor:


miTerritorio = new Territorio( width , height , celdas , celdas)
Luego, recorre todos los organismos, ejecutando la acción ubicar( ), para que el territorio los ubique en la celda que
les corresponde.
La función resolver_Encuentros_Organismos( ) es algo más compleja:

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;

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<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() )

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<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.

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.

You might also like