La primera pregunta obvia que puede aparecer al estudiar este tema es: “¿Qué es un sistema de partículas y una partícula?”
El término “sistema de partículas” ha sido utilizado comúnmente en técnicas de diseño y generación de gráficos por
computadora. Según Williams Reeves –un investigador de Lucas Films– un sistema de partículas es una colección de
muchos objetos, que se denominan partículas, y que en conjunto representan un objeto difuso.
La palabra Vector que antepone a cada operación se utiliza solamente a modo ilustrativo. En la operatoria normal, en lugar de la palabra vector se pone el nombre del PVector con el cual se desea realizar la operación. Por ahora es necesario quedarse con la idea de que un sistema de partículas está compuesto por objetos que comparten algún tipo de relación entre sí. Es muy útil por ejemplo pensar en la arena. Si se tuviera la oportunidad de observar los componentes de la arena, se vería una cantidad de pequeñas rocas muy diferentes entre sí y con diferentes formas, pero que en conjunto conforman lo que se entiende como arena. Cada una de esas diminutas rocas es una partícula, definiendo por lo tanto a una partícula como la mínima unidad que conforma el sistema, y la cual posee características que la relacionan con las otras unidades, pero que también las diferencia. Así, se pueden utilizar sistemas de partículas para generar otras texturas, como agua, tierra, fuego, o cualquier cosa que tenga las características antes mencionadas.
Durante el paso del tiempo, estas partículas se generan en un sistema, se mueven y cambian dentro de este sistema
y pueden morir, eventualmente, dentro del mismo sistema. Esto último, particularmente, se relaciona directamente
con los comportamientos que se le otorguen a cada una de nuestras partículas.
Desde hace más de tres décadas los sistemas de partículas han sido utilizados en animaciones, videojuegos, piezas
de arte digital, etc. Por eso, es necesario revisar y entender las diferentes formas de crear un sistema de partículas, y
en este caso utilizando Processing. En primera instancia el aprendizaje se centrará en cómo construir una partícula
y replicarla para generar cientos o miles de estas. Para esto, se trabajará con los conceptos de clase y objeto, parte
importante de la programación orientada a objetos.
Para refrescar la memoria, se revisarán los conceptos de clase y objeto que se explicaron en la unidad 2.
Se dijo que la expresión “clase” refiere a una construcción conceptual que combina datos y manipuladores de estos
datos. Dentro de una clase se tienen variables, a las cuales se denominan “campos”, y funciones a las cuales se denominan
“métodos”. En esta unidad se entenderá que cada clase puede tener instancias, o sea, aplicaciones de sí misma,
y que estas instancias son denominadas objetos.
A grandes rasgos, la clase plantea y contiene el concepto, los datos y los manipuladores y los objetos son las aplicaciones directas de esos conceptos, datos y manipuladores. La instancia es la relación entre la clase y el objeto, y se denomina instanciación al paso de clase a objeto.
El programa de ejemplo de la unidad 2 (2.11.) solo dibujaba un círculo, una partícula única. Si se desean crear muchos
objetos de una misma clase, se deberían definir múltiples objetos Círculo, luego construir cada uno por separado,
etc. Esto produciría una gran cantidad de código, haciendo engorroso el desarrollo del programa. Para evitar esto, se
utilizarán herramientas de programación que se han aprendido en este curso, los arrays y los loops. Se crearán arrays
de objetos, para luego ir generando y otorgando datos a partir de un loop. Para crear un array de objetos de una clase
se realizan los siguientes pasos:
1 – Se define el array de objetos.
2 – Se indica el tamaño del array que se utiliza.
3 – Se construyen los objetos dentro de un loop y se asignan los valores a los campos de forma secuencial.
A continuación, un ejemplo aplicado que utiliza la clase Círculo:
//Se define una variable global para la cantidad de círculos que se crean. En
//este caso serán 10 círculos, pero pueden ser más.
int numeroCirculos = 10;
//Paso 1 y Paso 2, se define un array de objetos de la clase Circulo. Esto se indica
//mediante corchetes. En la misma línea se indica el tamaño que tendrá el array
//utilizando la variable numeroCirculos.
//Esta sintaxis es similar a la del constructor del objeto, pero es parte de la
//asignación del array, no se deben confundir.
Circulo[ ] uno = new Circulo[10];
//Se inicializa el programa mediante setup
void setup()
{
//Tamaño de la ventana
size(100, 100);
//Mediante un loop for se van creando variables para los campos, y
//construyendo cada objeto del array uno. Como se observa se utiliza el
//tamaño del array como límite del loop. Esto evita errores de acceso a
//memoria
for (int i = 0; i < uno.length; i++)
{
//Las variables x e y están vinculadas al índice del array por lo que las
//posiciones irán aumentando a medida que pasa el tiempo
int x = 10 * i;
int y = 10 * i;
int diametro = 10;
//Paso 3, por último se construyen los objetos indexando el array con el índice i
//del loop
uno[i] = new Circulo(x, y, diametro);
}
}
//Loop de dibujo
void draw()
{
background(0);
//Para llamar a los métodos de cada objeto se utiliza un loop for
for (int i = 0; i < uno.length; i++)
{
uno[i].mostrarCirculo();
}
}
De esta manera, se puede generar la cantidad de objetos que se desea, y cada uno puede tener una característica que
lo diferencia para obtener un sistema de partículas.
Estos ejemplos muestran cómo generar un sistema de partículas muy sencillo. De aquí en más se revisarán estrategias
para generar sistemas de partículas más complejos. Se utilizarán las nociones de movimiento que se han utilizado y
el uso de Pvector´s que dotarán al programa de un manejo más fluido. También se incursionará en las listas de array
(ArrayList) las cuales permiten definir sistemas de partículas dentro de cada clase. Por último, se definirán dos principios
fundamentales de la programación orientada a objetos que serán de mucha ayuda en el trabajo con sistemas de
partículas: la herencia y el polimorfismo.
La idea es empezar a construir sistemas de partículas que sean más interesantes. En esta primera instancia se utilizarán
las nociones de movimiento mediante el uso de vectores.
Para empezar, es necesario establecer una clase denominada “Partícula”. Esta partícula será, como en los ejemplos
anteriores, un elemento, un cuerpo que se mueve de forma independiente por la pantalla. Por lo tanto, contendrá
vectores para su ubicación, velocidad y aceleración:
class Particula
{
PVector ubicacion;
PVector velocidad;
PVector aceleracion;
//constructor recibe un vector y obtiene y genera los campos
//necesarios
Particulas(PVector L)
{
ubicacion = L.get();
aceleracion = new PVector();
velocidad = new PVector();
}
void actualizar()
{
velocidad.add(aceleracion);
ubicacion.add(velocidad);
}
void mostrar()
{
stroke(0);
fill(175);
ellipse(ubicacion.x,ubicacion.y,8,8);
}
}
A esta partícula simple se le puede agregar campos que trabajen sobre el color, o también relacionarla con una determinada
imagen. Pero por ahora, para no complicar el desarrollo de la aplicación el enfoque estará puesto en la
estructura de la misma. Se agrega un campo más a la clase que será el de duración de la partícula, que dará la opción
de darle un tiempo de vida a cada partícula y generar determinados comportamientos interesantes relacionados con
la visualización o no de las mismas. Este campo se llamará “duración”. Esta variable se creará como campo de la clase
y luego aparecerá en cada una de los métodos utilizados:
class Particula
{
PVector ubicacion;
PVector velocidad;
PVector aceleracion;
float duracion;
//constructor recibe un vector y obtiene y genera los campos
//necesarios
Particulas(PVector L)
{
ubicacion = L.get();
aceleracion = new PVector();
velocidad = new PVector();
//utilizamos un valor de vida arbitrario.
duración = 255;
}
void actualizar()
{
velocidad.add(aceleracion);
ubicacion.add(velocidad);
//En cada actualización se resta una cantidad a la duración
duración -= 2.0;
}
void mostrar()
{
//Se afectan las opciones de dibujo mediante el agregado
//del campo duración como valor de transparencia. Esto
//proporcionará el efecto de que la partícula desaparece.
stroke(0,duracion);
fill(175,duracion);
ellipse(ubicacion.x,ubicacion.y,8,8);
}
//Se agrega un nuevo método que nos indica si la partícula sigue visible o
//no. Esto permitirá a futuro ahorrar espacio de memoria, ya que las
//partículas que no existan más, dejarán su espacio para las nuevas
//partículas. También se asegurará que el valor de duración no baje por //debajo de cero
boolean noExiste()
{
If(duración <0)
{
return true;
}
else
{
return false;
}
}
//También se agrega una función que permita darle la orden a la
//partícula para que aparezca y comience su período de vida
void empezar()
{
actualizar();
mostrar();
}
}
La aplicación en un programa de esta clase partícula sería la siguiente:
Particula p;
void setup()
{
size(200,200);
p = new Particula(new PVector(width/2,10));
smooth();
}
void draw()
{
background(255);
p.empezar();
if (p.noVisible())
{
println(“La partícula no existe más!”);
}}
Se ha visto que es relativamente fácil generar un sistema de partículas utilizando un array de objetos. Sin embargo, existe
un tipo de array que permite ser más conciso, simple y elegante en la estructura y generación del sistema de partículas,
debido a que se redimensiona automáticamente cuando se agrega una partícula, sin tener que preocuparse por la nueva
asignación de memoria. Este tipo de array se denomina ArrayList y con él se pueden generar tantas partículas como
se requiera (o la memoria de la computadora permita) sin preocuparse por cambiar la cantidad de objetos una y otra
vez. ArrayList genera un flujo de partículas constante que va creciendo sin límite. Afortunadamente, existe la posibilidad
de ir removiendo las partículas que ya no están en uso, las que ya no se visualizan, y de esta manera mantener un ciclo
constante de partículas sin sobrecargar el sistema. La sintaxis responde a las siguientes reglas:
Para poder utilizar los ArrayList primero se debe importar la librería correspondiente:
import java.util.Iterator;
Esta librería de utilidades de Java, dota a Processing de las funcionalidades necesarias para lograr las iteraciones que
se proponen con los ArrayList.
A continuación, primero se define el ArrayList que utiliza la clase Partícula, y que se llamará, en este ejemplo, partículas:
ArrayList<Particula> particulas
Luego, en el setup, se asigna el espacio inicial correspondiente:
particulas = new ArrayList<Particula>();
En el espacio del draw se comienzan a agregar partículas a la lista mediante el método add, y pasándole como argumento
el vector de la posición inicial, en este caso para cada loop de draw las partículas nacerán en la misma ubicación:
particulas.add(new Particula(new PVector(width/2,50)));
Para el paso siguiente, en donde se ejecutarán los métodos de cada partícula, se debe tener en cuenta que los ArrayList permiten generar un iterador (repetidor), el cual va recorriendo secuencialmente cada elemento del array, tenga
el tamaño que tenga, sin importar si se agregan o quitan elementos. De esta manera, no hay que preocuparse de
cometer un error a la hora de indexar los loops:
Iterator<Particula> it = particulas.iterator();
Además, se utiliza la función hasNext(), que indica la existencia de una partícula, para la cual se corren las funciones
propias. Se agrega también la opción de que se fije si la partícula está todavía visible o ya no existe más, en cuyo caso
será removida del ArrayList. Mediante la variable “it” se está indicando que se realizará la acción para el Iterator creado.
Un ejemplo para clarificar:
while (it.hasNext())
{
//Se asigna a cada nueva partícula del iterador a una
//partícula que funciona de contenedor temporal.
Particula p = it.next();
//Se ejecuta el método que empieza el ciclo de cada
//partícula.
p.empezar();
//Si la particula ya no está visible, se remueve.
if (p.noVisible())
{
it.remove();
}}
De esta manera, el código del programa se reduce exponencialmente, dejando una sintaxis bastante limpia y funcional.
Sin embargo, existe una forma de que la sintaxis sea aun más acotada, mediante la creación de una nueva clase
que contenga el ArrayList y sus funcionalidades. Para ejemplificar se llamará a esta nueva clase SistemaDeParticulas:
class SistemaDeParticulas
{
//Se define el ArrayList que se utilizará
ArrayList particulas;
//Se agrega a este sistema de partículas un punto de origen de cada
//uno de los elementos
PVector origen;
//Constructor al cual se le pasa la ubicación de cada partícula
SistemaDeParticulas(PVector ubicacion)
{
//Se define el origen
origen = ubicacion.get();
//Se asigna el espacio para las partículas
particulas = new ArrayList();
}
//Método para agregar partículas al ArrayList
void AgregarParticula()
{
//Se crean las partículas invocando a la clase
//Partícula
particlulas.add(new Particula(origen));
}
//El método a empezar ahora contiene todas las opciones del iterador
void run()
{
Iterator<Particula> it = particulas.iterator();
while (it.hasNext())
{
Particula p = it.next();
p.run();
if (p.noVisible ())
{
it.remove();
}
}
}
Una de las formas de utilizar esta nueva clase en el programa principal sería la siguiente:
//Se define un nuevo sistema de partículas.
SistemaDeParticulas SP;
void setup()
{
size(800,200);
smooth();
//Se construye se pasa la ubicación de las partículas.
SP = new SistemaDeParticulas(new PVector(width/2,50));
}
void draw()
{
background(255);
//Se agregan las partículas en cada cuadro.
SP.addParticle();
//Se comienza a ejecutar cada partícula.
SP.empezar();
}
De esta manera se ha reducido la generación del sistema de partículas a una sintaxis mínima que permite tener un
código ordenado y claro.
A continuación, un ejemplo complementario de la aplicación de
la clase partícula y un array list:
CAP5EjemploComplementario1.pde
Actividad 1
Crear un programa que muestre 4 sistemas de partículas con diferentes elementos, utilizando Array List. Los 4 sistemas deben moverse por la acción del mouse, pero no todos en la misma dirección.
Cuando se habla de herencia en un lenguaje de programación orientado a objetos, como lo es Processing, se dice que
esta característica permite escribir una clase que herede variables y funciones de otra u otras clases previamente escritas,
además de mantener las funcionalidades propias. De esta manera, se puede reutilizar código constantemente
sin tener que escribir características similares en diferentes clases.
Un ejemplo clásico es una clase que describe los comportamientos de un determinado animal, puede ser un perro: un
perro tiene una edad, come, duerme, camina, etc. Escribimos cada uno de los campos y métodos para la clase perro,
y así podemos crear objetos con estas características.
Pero ¿qué sucede si se necesita crear otra clase, pero que describa a otro animal? Una solución es escribir toda la clase
de nuevo, la cual contendrá campos y métodos similares, describiendo características y acciones propias de familias
de animales.
Y ¿qué sucede si se deben crear clases para cientos de animales de diferentes razas? Para solucionar este problema
existe la herencia. Como se dijo anteriormente, se pueden heredar campos y métodos de una clase a otra, compartiendo
funcionalidades, pero también pudiendo escribir características propias.
Para resolver el problema de cómo escribir clases de animales, se puede encarar el mismo escribiendo una clase
madre llamada “animales”. Esta clase contendrá características comunes a los animales: edad, pelaje, caminar, correr,
dormir, etc. que pueden ser fácilmente heredadas. En la programación orientada a objetos, esto se denomina superclase
(superclass), y una clase que hereda características de esta superclase es una subclase (subclass), por ejemplo las
clases “gato”, “perro”, “vaca”, etc., serían herederos de la superclase “animales”.
Dentro de Processing la sintaxis para cumplir con una correcta herencia de características se da mediante la utilización
de la palabra clave extends, que indica que la nueva clase es una extensión de otra clase y de un constructor
denominado super(), el constructor de la clase padre, que indica que todo lo que se hace en el constructor padre se
realice en el constructor hijo también.
Para aclarar el uso de sintaxis, a continuación se presenta un ejemplo de la misma:
Primero la superclase animal:
class animal
{
//Campos
int edad;
//Constructor de la clase
Animal()
{
edad = 0;
}
//Método comer
void comer()
{
println(“yum!”);
}
//Método dormir
void dormir()
{
println(“zzzzzz”);
}
}
Luego, se crea una clase llamada, por ejemplo, “gato”, la cual heredará las características de la superclase animal. Se
observa claramente cómo se utilizan las opciones de extends y super():
class gato extends animal
{
//Se agrega un campo propio de esta clase
Color colorDePelo;
//Constructor de la clase gato
gato()
{
//Constructor de la superclase
super();
}
//Sumo un método adicional
Void sonido()
{
println(“Miau!”);
}
}
Otra característica interesante de la herencia es que una subclase puede utilizar y ejecutar campos y métodos de la
superclase. Por ejemplo, esto podría realizarse de la siguiente manera:
class gato extends animal
{
//Constructor de la clase gato
gato()
{
//Constructor de la superclase
super();
}
//Genero un método adicional, pero llamando a un método de la
//superclase
Void comer()
{
Super.comer();
}
}
A continuación, se revisará un ejemplo complementario que
muestra un sistema de partículas completo, el cual utiliza conceptos
de herencia. Se añade una clase que genera directamente
un sistema de partículas, y la extensión de estas clases por parte
de otra clase, completando el concepto de herencia:
CAP5EjemploComplementario2.pde
El polimorfismo, siempre dentro de la teoría de la programación orientada a objetos, refiere a un conjunto de clases
que poseen campos y métodos en común, por lo cual, existe la posibilidad de enviar un solo mensaje para ejecutarlos
todos juntos. Formalmente, se puede definir como polimorfismo al tratamiento de una sola instancia de un objeto
de muchas formas. Esto es muy útil cuando se tiene una superclase y muchas subclases. Por ejemplo, si tenemos la
superclase “animal” cuyas subclases son perro, gato, canario, etc. y queremos generar muchos objetos a partir de
estas subclases, podemos hacer que todas respondan a la acción “comer” mediante la utilización del concepto de
polimorfismo.
Si tenemos un ArrayList del tipo de clase “animal”, podemos llenarlo con diferentes tipos de subclases, ya que estas
son extensiones de la superclase y no habrá problemas de tipo. El ejemplo siguiente crea el ArrayList, lo llena con las
diferentes subclases y ejecuta un método compartido por todas las clases:
//Se crea un ArrayList del tipo “animal”
ArrayList<Animal> ReinoAnimal = new ArrayList<animal>();
//Mediante un loop for se llena el array con diferentes tipos de animales
for (int i = 0; i < 1000; i++)
{
if (i < 100) ReinoAnimal.add(new perro());
else if (i < 400) kingdom.add(new gato());
else if (i < 900) kingdom.add(new canario());
else ReinoAnimal.add(new tortuga());
}
//Se ejecuta mediante un loop for el método “comer” para todos los
//animales
for (animal a: ReinoAnimal)
{
a.comer();
}
De esta forma se crea un conjunto de clases diferenciadas, que además comparten características de herencia y se
puede ejecutar acciones comunes sin necesitar demasiadas líneas de código.
Un ejemplo complementario muestra de qué forma se puede
combinar herencia y polimorfismo en un sistema de partículas.
En este caso no se utilizan array list para que se entiendan los conceptos
básicos:
CAP5EjemploComplementario3.pde
Actividad 2
A partir de los ejemplos complementarios, generar un sistema de partículas donde se utilicen los conceptos de herencia y polimorfismo, pero con imágenes cargadas al programa. Es tarea del estudiante determinar qué elementos serán heredados y cómo se aplica el polimorfismo. Se trata de una actividad de creación propia de un programa informático con base en sistemas de partículas.
.