Comunidad de diseño web y desarrollo en internet online

Efecto de ondas con malla en AS3

El objetivo de este tutorial es hacer un efecto de ondas con malla en AS3, que consista en realizar un grupo de cuadrados negros, ordenados en una cuadrícula. Cuando arrastro un cuadrado con el ratón, los demás cuadrados también se van a mover, como si fuese una red elástica. Cuando se suelte, esta va a rebotar y ondear un poco, hasta detenerse.

Diseñar el programa


Es importante tener una cierta idea de la estructura del programa antes de empezar con los detalles, ya que si no sabrás qué clases hacer ni por dónde comenzar.

Por ejemplo, en éste caso el programa va a ser bastante simple. Vamos a tener un sistema y varios cuadrados. El sistema controla los cuadrados, y éstos se relacionan con sus correspondientes. Vamos a guardar a los cuadrados en un gran vector bidimensional, por filas y columnas. Para cada cuadrado, va a haber un enlace "elástico" con los cuadrados que estén inmediatamente al lado de ellos; arriba, abajo, a la derecha e izquierda. Quizá los hagamos también con diagonales.

Los cuadrados de las orillas no se van a mover, ni lo va a hacer el cuadrado seleccionado por el ratón (ya que va a seguir al ratón). Los demás se dejarán llevar por las fuerzas elásticas de sus enlaces. Estos van a ser los "activos", los demás van a ser "inactivos". Quizá haya fricción, para parar las ondulaciones después de cierto tiempo.

Entender la física detrás del programa


Fuerza es igual a la masa por la aceleración, y la fuerza elástica es igual al coeficiente de elasticidad por la extensión entre la distancia normal.

Como sólo lo estamos simulando, y los cuadrados en realidad no tienen masa, diremos que la aceleración va a ser igual al la extensión entre la longitud natural, todo multiplicado por una constante que manejaremos a antojo. Es importante notar que esto se puede extender fácilmente a las componentes 'x' e 'y' de la aceleración.

Ya estoy pensando en un algoritmo para calcular la aceleración, teniendo cuadrado1 y cuadrado2:

Código :

// Usemos pitágoras de toda la vida!
distancia = Math.sqrt(( cuadrado1.x - cuadrado2.x ) * ( cuadrado1.x - cuadrado2.x ) + ( cuadrado1.y - cuadrado2.y ) * ( cuadrado1.y - cuadrado2.y )); 

// Calculamos la extensión, puede ser positiva o negativa, en cuyo caso sería una compresión
extensión = distancia - distanciaNatural;

// Calculamos factor de conversión Distancia -> Aceleración
factor = constante * extensión / distanciaNatural;

// Calculamos la aceleración en los componentes XY
accX = factor * ( cuadrado1.x - cuadrado2.x );
accY = factor * ( cuadrado1.y - cuadrado2.y );



Con los conceptos matemáticos y físicos al día, crear algoritmos de este tipo no es demasiado difícil. Si no, con un poco de práctica se puede conseguir.

Comenzando el programa


Creamos un nuevo documento en FlashDevelop (en mi caso).

Código :

package 
{
   import flash.display.Sprite;
   import flash.events.Event;
   
   /**
    * ...
    * @author Agecaf
    */
   public class Main extends Sprite 
   {
      
      public function Main():void 
      {
         if (stage) init();
         else addEventListener(Event.ADDED_TO_STAGE, init);
         
      } // Final de constructor
      
      private function init(e:Event = null):void 
      {
         removeEventListener(Event.ADDED_TO_STAGE, init);
         // entry point
         
      } // Final de función init
      
   } // Final de clase Main
   
} // Final de package


Con tan sólo crear el proyecto, FlashDevelop hace ya mucho código automático. Ahora bien, ¡comenzemos a crear nuestro código!

Primero, añadimos las propiedades que vamos a usar, que van a ser el Vector de objetos y... nada más.

Código :

private var cuadrados:Vector.<Vector.<Cuadrado>>;


Todavía no hemos creado la clase "Cuadrado", pero la haremos después. Como hemos diseñado bien el código, tenemos una buena idea de cómo va a ser esa clase, y cómo vamos a usarla.

Ahora bien, lo segundo que vamos a hacer es la función "alEntrarAFrame".

Código :

private function init(e:Event = null):void 
{
   removeEventListener(Event.ADDED_TO_STAGE, init);
   // entry point
   
   // Escucha Eventos
   this.addEventListener( Event.ENTER_FRAME, alEntrarAFrame );
   
} // Final de función init

private function alEntrarAFrame(e:Event):void 
{
   for ( var i:int = 0; i < cuadrados.length; i++ ) // Recorre filas
   {
      for ( var j:int = 0; j < cuadrados.length; j++ ) // Recorre columnas
      {
         // Actualiza todos los cuadrados
         cuadrados[ i ][ j ].actualizar();
         
      } // Final de for columnas
      
   } // Final de for filas

   for ( i = 0; i < cuadrados.length; i++ ) // Recorre filas
   {
      for ( j = 0; j < cuadrados.length; j++ ) // Recorre columnas
      {
         // Mueve todos los cuadrados
         cuadrados[ i ][ j ].mover();
         
      } // Final de for columnas
      
   } // Final de for filas

   
} // Final de función alEntrarAFrame



Primero, añadimos un escuchador de eventos para el inicio de cada frame. Luego, usamos eso para actualizar todos los cuadrados en cada frame, y luego usamos eso para mover todos los cuadrados. Es importante moverlos después de haberlos actualizado pues la actualización depende de la posición de los demás cuadrados.

Para establecer el programa, necesitamos primero crear los cuadrados, y luego enlazarlos unos con otros. Además, hay que "activar" los cuadrados que no están en las orillas.

Añadimos a nuestra función init

Código :

// Crea Cuadrados
cuadrados = new Vector.<Vector.<Cuadrado>>(); // Crea el conjunto
cuadrados.length = 75;
for ( var i:int = 0; i < cuadrados.length; i++ )
{
   cuadrados[ i ] = new Vector.<Cuadrado>(); // Crea fila
   cuadrados[ i ].length = 100;
   
   for ( var j:int = 0; j < cuadrados[ i ].length; j++ )
   {
      // Crea Cuadrado
      cuadrados[ i ][ j ] = new Cuadrado();
      
      // Ajusta Posición del Cuadrado
      cuadrados[ i ][ j ].x = j * 20;
      cuadrados[ i ][ j ].y = i * 20;
      
      // Añade Cuadrado
      addChild( cuadrados[ i ][ j ] );
   }
}


Que crea los cuadrados y los posiciona como es debido, además de añadirlos a la pantalla. Luego los enlazamos así:

Código :

// Enlaza Cuadrados
for ( i = 1; i < cuadrados.length - 1; i++ )
{
   for ( j = 1; j < cuadrados.length - 1; j++ )
   {
      // Enlaza cuadrados
      cuadrados[ i ][ j ].enlazarA( cuadrados[ i     ][ j + 1 ] );
      cuadrados[ i ][ j ].enlazarA( cuadrados[ i + 1 ][ j     ] );
      cuadrados[ i ][ j ].enlazarA( cuadrados[ i     ][ j - 1 ] );
      cuadrados[ i ][ j ].enlazarA( cuadrados[ i - 1 ][ j     ] );
      
      // Activa cuadrados
      cuadrados[ i ][ j ].activar();
   }
}


Que enlaza cada cuadrado que no está en una orilla a los cuadrados de alrededor. Además, los activa.

Os habréis dado cuenta de que todavía no hemos hecho la clase "Cuadrado", pero que aún así estamos usándolo como si ya lo conociéramos. Básicamente, nos estamos diciendo a nosotros mismos qué necesitamos de esta clase, como por ejemplo un método "actualizar", uno "activar", y uno "enlazarA". Además, como usa eventos del ratón, posición y tiene imagen, sabemos que debemos utilizar un Sprite como su base.

Pues, a hacer Cuadrados se ha dicho!

Crear un Objeto con restricciones ya conocidas


¡Por fín creamos nuestra clase Cuadrado! Para esto, FlashDevelop es muy amable y nos hace un poco del código.

Código :

package  
{
   import flash.display.Sprite;
   
   /**
    * ...
    * @author Agecaf
    */
   public class Cuadrado extends Sprite 
   {
      
      public function Cuadrado() 
      {
         super();
         
      } // Final de Constructor
      
   } // Final de clase Cuadrado

} // Final de package


Primero, ¡hay que dibujar el cuadrado! Añadimos entonces este código al constructor:

Código :

// Dibuja cuadrado
graphics.beginFill( 0x000000 );
graphics.drawRect( -5, -5, 10, 10 );
graphics.endFill();


Ahora, añadimos las propiedades fundamentales de nuestro Cuadrado. Estas serían las propiedades físicas, como la velocidad y la aceleración, y las propiedades enlazantes, como los cuadrados con los que está enlazado y sus distancias naturales. Además, ¡tenemos la constante misteriosa!

Código :

// Base
private const constante:Number = -0.1;
private var activo:Boolean = false;
private var presionado:Boolean = false;

// Propiedades Físicas
private var velX:Number = 0.0;
private var velY:Number = 0.0;
private var accX:Number = 0.0;
private var accY:Number = 0.0;

// Enlazes
private var cuadradosEnlazados:Vector.<Cuadrado> = new Vector.<Cuadrado>();
private var distanciasNaturales:Vector.<Number> = new Vector.<Number>();


Una vez hecho esto, podemos empezar a añadir uno a uno los métodos que habíamos hecho. Empecemos por los fáciles:

Código :

public function activar():void { activar = true; return; }


Bueno... Este fue muy fácil y breve. Sigamos con... ¡mover! ¡Ésa también ha de ser fácil!

Código :

public function mover():void
{
   // Cabia velocidad
   velX += accX;
   velY += accY;
   
   // Cambia Posición
   x += velX;
   y += velY;
   
} // Final de función mover


Creo que no hace falta explicar ése código... Bueno, ahora empieza lo divertido... ¡a "enlazar" se ha dicho!

Código :

public function enlazarA ( cuadrado:Cuadrado ):void
{
   // Añade cuadrados a la lista
   cuadradosEnlazados.push( cuadrado );
   
   // Calcula Distancia Natural por Pitágoras
   var distancia:Number = Math.sqrt(( x - cuadrado.x ) * ( x - cuadrado.x ) + ( y - cuadrado.y ) * ( y - cuadrado.y )); 
   
   // Añade distancia natural a la lista
   distanciasNaturales.push( distancia );
}


Creo que esto también queda claro... sólo hace falta conocer la función push de los vectores (añade un elemento al final), el teorema de Pitágoras, y saber leer comentarios.

¡Ahora viene Lo más divertido! Usar por fin nuestras fórmulas que encontramos en el paso dos o tres, ya ni me acuerdo.

Código :


public function actualizar():void
{
   // Resetea Aceleración 
   accX = 0.0;
   accY = 0.0;
   
   if ( activo )
   {
      for ( var i:int = 0; i < cuadradosEnlazados.length; i++ )
      {
         // Llama cuadrado a cuadradosEnlazados[ i ]
         var cuadrado:Cuadrado = cuadradosEnlazados[ i ];
         
         // Calcula distancia con pitágoras
         var distancia:Number = Math.sqrt(( x - cuadrado.x ) * ( x - cuadrado.x ) + ( y - cuadrado.y ) * ( y - cuadrado.y ));
         
         // Calcula extensión
         var extensión:Number = distancia - distanciasNaturales[ i ];
         
         // Calcula factor de conversión
         var factor:Number = constante * extensión / distanciasNaturales[ i ];
         
         // Acelera
         accX += factor * ( x - cuadrado.x );
         accY += factor * ( y - cuadrado.y );
         
      } // Final de for cuadrados
      
   } // Final de si está activo
   
} // Final de función actualizar


Parece complicado, pero es básicamente la fórmula que ya habíamos obtenido. Añadimos el filtro de si está o no activo para que los bordes se queden quietos.

Escuchar al ratón


Finalizaremos esto con un par de eventos del ratón; primero, en nuestro constructor:

Código :


// Escucha Eventos
addEventListener(MouseEvent.MOUSE_DOWN, alPresionar );
addEventListener(MouseEvent.MOUSE_UP,   alLevantar  );



Luego implementamos estas funciones muy simples...

Código :

private function alPresionar ( e:MouseEvent ):void
{
   // Presionado
   presionado = true;
   activo = false;
}

private function alLevantar ( e:MouseEvent ):void
{
   // Se reactiva
   activar();
   presionado = false;
}


Y añadimos algo en alEntrarAFrame

Código :

} // Final de si está activo

else if ( presionado )
{
   velX = 0;
   velY = 0;
   
   x = stage.mouseX;
   y = stage.mouseY;
   
}


Que básicamente mueve el cuadrado a la posición de nuestro ratón.


Admirar Nuestra Creación.





No está mal... ¿Pero tampoco es lo que nos esperábamos? Se mueve mucho, y nunca se está quieto. Además, se puede "romper" si se agita muy velozmente... Y, para empeorarlo un poco, a veces se "atora" un cuadrado con su diagonal, ya que no hay enlace entre ellos...

Pero, ¿Qué podemos hacer para solucionar esto? Usar la idea que teníamos desde el paso 3 o 2.

Fricción. Eso lo alentecerá un poco. ¿Cómo lo conseguimos?, creo que cargándonos un poco de velocidad cada frame.

Este es el truco de los programadores para la fricción, que daría escalofríos a cualquier físico. ¡Multiplicamos la velocidad por 0.9, or 0.99 en cada frame!

Añadimos esto a "mover":

Código :

// Fricción
velX *= 0.99;
velY *= 0.99;


Usar nuevas ideas


Para evitar que se rompa, que ocurre cuando algo se mueve muy rápido... ¿Por qué no tener aceleraciones y velocidades terminales? Así las partículas nunca irán lo suficientemente rápido para romperse. El código sería el siguiente:

Código :

if ( accX * accX + accY * accY > 20 )
{
   factor = Math.sqrt(( accX * accX + accY * accY ) / 20);
   accX /= factor;
   accY /= factor;
}


Y algo similar para la velocidad. Lo colocamos de tal manera que quede antes de usar los valores, por ejemplo, acotamos la velocidad entre que la cambiamos y cambiamos la posición.

Al final queda algo parecido a esto en la función mover:

Código :

public function mover():void
{
   var factor:Number = 1.0;
   
   // Aceleración Terminal
   if ( accX * accX + accY * accY > 20 )
   {
      factor = Math.sqrt(( accX * accX + accY * accY ) / 20);
      accX /= factor;
      accY /= factor;
   }
   
   // Cabia velocidad
   velX += accX;
   velY += accY;
   
   // Velocidad terminal
   if ( velX * velX + velY * velY > 40 )
   {
      factor = Math.sqrt(( velX * velX + velY * velY ) / 40);
      velX /= factor;
      velY /= factor;
   }
   
   // Cambia Posición
   x += velX;
   y += velY;
   
   // Fricción
   velX *= 0.99;
   velY *= 0.99;
   
} // Final de función mover


Añadir enlaces entre diagonales


Este paso es muy simple... simplemente escribimos un par de líneas que deberíamos haber escrito pero eramos demasiado tacaños para hacerlo. Donde enlazamos los cuadrados (en Main), añadimos cuatro líneas de código:

Código :

cuadrados[ i ][ j ].enlazarA( cuadrados[ i     ][ j + 1 ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i + 1 ][ j     ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i     ][ j - 1 ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i - 1 ][ j     ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i + 1 ][ j + 1 ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i + 1 ][ j - 1 ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i - 1 ][ j - 1 ] );
cuadrados[ i ][ j ].enlazarA( cuadrados[ i - 1 ][ j + 1 ] );


Admirar nuestra creación y aprender de nuestro programa





Ahora sí. No se rompe, y tenemos ondas muy bonitas por toda la pantalla. Estamos orgullosos de nuestro pequeño programa, y hemos aprendido cómo hacer un efecto especial considerando y construyendo las partes de manera sistemática, diseñando, usando cosas que todavía no habíamos creado, y puliendo los detalles finales para evitar fallos presentes en nuestro programa.

En conclusión, para crear un programa desde cero, Hay que
  1. Diseñar
  2. Construír
  3. Rellenar
  4. Pulir


Soñar en pasos futuros...


¿Ahora qué?
Pues ya se me han ocurrido mil ideas que hacer con este programa, pero esto os lo dejo a ustedes. Por ejemplo, ¿qué pasaría si uno de los bordes se moviera periódicamente, generando ondas? ¿Qué pasaría si dejásemos puntos inmóviles dentro de la cuadrícula? ¿Qué pasaría si, en vez de estar unidos a los cuadrados de al lado, cada cuadrado estuviese unido a todos y cada uno de los demás cuadrados? ¿Y si no hubiese borde estático?

¡Pero todas estas ideas van a tener que ser para otro día!

Descarga las clases utilizadas

¿Sabes SQL? ¿No-SQL? Aprende MySQL, PostgreSQL, MongoDB, Redis y más con el Curso Profesional de Bases de Datos que empieza el martes, en vivo.

Publica tu comentario

o puedes...

¿Estás registrado en Cristalab y quieres
publicar tu URL y avatar?

¿No estás registrado aún pero quieres hacerlo antes de publicar tu comentario?

Registrate