Quick Links

Education Edu

Simulations Sims

Math Tools Math

Games Games

Generative ArtArt

Actinoscript Prog

Farmville Farm

 

 


Adjustable Blob

Adjustable Blob

Click the above-left picture of the adjustable blob applet to start the application in a new window.

 
 

Animated Blob Creation In Actionscript 3

While writing the amoeba vs. bacteria game, I had the opportunity to program a blob. By my definition, a blob is a collection of interconnected points in the shape of a circle, but each point can move independently of the others. The points, when out of position, tend to revert toward their default radius and the average of their neighbors on either side. In my game, the blob attempts to follow the mouse using both acceleration and velocity, but, from the blob's center, the points that are in the direction that the mouse moves, move more than those that are in the opposite direction. Also, under certain situations, the edge points get some degree of randomness.

So, summarizing what I wanted to see:
. points revert to radius
. points average with neighbors
. points in mouse direction move farther
. points can get random wiggly-ness

I like to create tools to test out concepts. The blob had enough parameters that I decided to create a tool allow me to adjust them.

Result

I've got several sliders on the screen. The first two, vDamping, and friction apply to the motion of the blob as it follows the mouse. The second two, radius reversion & neighbor reversion, refer edge points tendency to even themselves out. Next, is the mouse desire. This refers to the degree to which the mouse-side edge of the blob gets pulled toward the mouse. The next two, wiggle diminishment & wiggle impact, affect how quickly the edges lose or increase their wiggly-ness. The wiggly-ness factor comes into play when the blob makes contact with the enemy bacteria. I've put a single bacterium in the upper right corner for testing purposes. The final slider adjust the radius of the blob.

Content on this page requires a newer version of Adobe Flash Player.

Get Adobe Flash player

History

I've wanted to make a simulation of microscopic organisms for some time. I'd previously created a golgi-volvox simulation which, though it looked relatively good, consumed an unreasonable amount of CPU. My intention for such a micro-simulation had been primarily to do some experimentation in artificial life/organisms that I had started eons ago. This blob however, was simply part of a game.

Prior Art

Nonetheless, I found substantial prior art that I ought to at least mention. A handful of people have created their own blobs that operate in the html5 arena. I think one of them is on google's chrome example page. Another person, Sophie Houlden (http://www.sophiehoulden.com/games/pinapple22/) created a similar blob-related game, from which I copied her idea of rectangles to show score type values.

Approach

The points around the edges of the circle are a bunch of sprites saved in an array. Each frame, I redraw the blob as an edgeless low opacity shape. This would probably take less resources if they were just Points, but I dislike the extra reference to the Point to get the values.

Restoring the points toward the standard radius is fairly simple: I just add the product of a reversionFactor (in my case 0.25) and the difference between the actual x and the radius. Same/same for in the y direction. Something akin to this:

xRad=radius*Math.cos(iP*Math.PI/180); eps[iP].x += reversionFactor*( xRad - eps[iP].x )

Similarly, after getting a weighted average of the point and it's two neighbors' x-s, I add the difference between it and the actual x, scaling it down by a neighborReversion factor of 0.083. Same for y. No special math here, just trial and error to get the scaling factor that seemed to work.

xRad=radius*Math.cos(iP*Math.PI/180); eps[iP].x -= neighborReversion*(eps[iP]-(eps[iP-1]+eps[iP]+eps[iP+1])/3)

The part about giving an extra push to the edge points that are in the direction of the mouse was a bit more troublesome. Naturally, I thought I'd just take the difference of the Math.atan2 angles for the mouse and a given edge point (both relative to the blob's center). But, because it is possible to position the mouse and edge point very near to one another but straddling the line where the angle jumps from +180 to -180, it can be difficult to figure out how far they are apart without considering some confusing edge conditions.

I actually failed at three other approaches before I finally just got lazy and sort of cheated by first determining the edge point nearest to the mouse angle and then giving a boost in the mouse direction to only the quarter of the points on either side of it. Not ideal, but it was a lot less confusing. One result of this less-than-idealness is that the blob tends to take on a rounded rectangle appearance at times.

Finally, there is the issue of the motion of the blob itself. This followed the mouse with a near verbatim copy of Natzke's accelerated easing methodolgy: Go Natzke!

x -= ddx = (ddX + (x-mouseX) * vDamp) * friction; y -= ddy = (ddY + (y-mouseY) * vDamp) * friction;

Code

Here is my SimpleBlob.as package.

package
{
import flash.display.MovieClip;
import flash.display.Sprite;
import flash.display.Graphics;
import flash.events.Event;
import flash.events.MouseEvent;

public class SimpleBlob extends MovieClip
{
// Constants:
// Public Properties:
// Private Properties:
private var pB:Sprite = new Sprite(); // point board
private var numCPs:Number=30; // # pts
private var numCPs4:Number=Math.floor(numCPs/4); // 1/4 of pts
private var cps:Array=[]; // control pts
private var cosX:Array=[]; // default Xs
private var sinX:Array=[]; // default Ys

private var defaultWall:Number=50;
private var rBlob:Number=50;

private var dX:Number=0; // velocity
private var dY:Number=0;

private var ddX:Number=0; // acceleration
private var ddY:Number=0;

private var margin:Number=10;
private var edgeLeft:Number;
private var edgeRight:Number;
private var edgeTop:Number;
private var edgeBottom:Number;

private var vDamp:Number=.11;
private var friction:Number=.55;

private var myParent:MovieClip;
private var sh:Number;
private var sw:Number;

private var radiusReversion:Number=0.25;
private var neighborReversion:Number=0.083;
private var mouseDesire:Number=0.25; // point factor
private var percentWiggle:Number=0; // increases w/enemy touches
private var wiggleDiminishment:Number=0.99; // rate of damage decay
private var wiggleImpact:Number=0.01;

private var pf:Array=[]; // pursuit factor

private var _blobColor:Number=0x0;

// Initialization:
public function SimpleBlob(passedInParent:MovieClip,_H:Number,_W:Number)
{
myParent=passedInParent;
sh=_H;
sw=_W;
initBlob();
edgeLeft=margin;
edgeRight=sw-margin;
edgeTop=margin;
edgeBottom=sh-margin;
addChild(pB);
addEventListener(Event.ENTER_FRAME,update);
}

private function update(e:Event=null):void
{
percentWiggle*=wiggleDiminishment;
dX=x-myParent.mouseX;
dY=y-myParent.mouseY;
x -= ddX = (ddX + dX*vDamp)*friction;
y -= ddY = (ddY + dY*vDamp)*friction;
x=Math.max(edgeLeft+2, Math.min(edgeRight -2, x) );
y=Math.max(edgeTop +2, Math.min(edgeBottom-2, y) );

// push edge points in direction of mouse move
var vA:Number=Math.round(numCPs*((360+Math.atan2(-dY,-dX)*180/Math.PI)%360)/360);
for(var iP:Number=Math.round(vA-numCPs4); iP<vA+numCPs4; iP++)
{
cps[(iP+numCPs)%numCPs].x-=mouseDesire*dX + .25*pf[ iP - Math.round(vA-numCPs4) ];
cps[(iP+numCPs)%numCPs].y-=mouseDesire*dY + .25*pf[ iP - Math.round(vA-numCPs4) ];
}
edgeAverage();
}
private function edgeAverage():void
{
for(var iP:Number=0; iP<cps.length; iP++)
{
wiggleRevertConfine(iP);
}
for(iP=0; iP<cps.length; iP++)
{
avgPositions(
(iP==0)?cps.length-1:iP-1,
iP,
(iP==cps.length-1)?0:iP+1
);
}
drawEdge();
}
private function wiggleRevertConfine(b:Number):void
{
// wiggle
var d:Number=dist(cps[b].x,cps[b].y);
if (Math.random()<percentWiggle)
{
cps[b].x+=d*(0.5-Math.random());
cps[b].y+=d*(0.5-Math.random());
}
// revert to radius
cps[b].x+=radiusReversion*(rBlob*cosX[b]-cps[b].x)
cps[b].y+=radiusReversion*(rBlob*sinX[b]-cps[b].y)

// keep w/in bounds
if (cps[b].x < edgeLeft -x) cps[b].x=edgeLeft -x;
if (cps[b].x > edgeRight -x) cps[b].x=edgeRight -x;
if (cps[b].y < edgeTop -y) cps[b].y=edgeTop -y;
if (cps[b].y > edgeBottom-y) cps[b].y=edgeBottom-y;
}
private function avgPositions(a:Number,b:Number,c:Number):void
{
// average with neighbors
cps[b].x+=neighborReversion*(cps[a].x+cps[c].x-2*cps[b].x);
cps[b].y+=neighborReversion*(cps[a].y+cps[c].y-2*cps[b].y);
}
private function dist(a,b):Number { return Math.sqrt(a*a+b*b); }
private function initBlob():void
{
// push edge points in direction of mouse move
var vA:Number=Math.round(numCPs*((360+Math.atan2(-dY,-dX)*180/Math.PI)%360)/360);
for(var iPF:Number=Math.round(vA-numCPs4); iPF<vA+numCPs4; iPF++)
{
pf[iPF-Math.round(vA-numCPs4)] = Math.pow((5+Math.abs(vA-iPF+.1)* (-numCPs*0.005/mouseDesire) ),1.25);
}

for(var iP:Number=0; iP<numCPs; iP++)
{
var cp:Sprite = new Sprite();
cps.push(cp);
cosX[iP]=Math.cos(iP*2*Math.PI/numCPs);
sinX[iP]=Math.sin(iP*2*Math.PI/numCPs);
cp.x=rBlob*cosX[iP];
cp.y=rBlob*sinX[iP];
}
drawEdge();
}
private function drawEdge():void
{
var g:Graphics=pB.graphics;
g.clear();
g.beginFill(_blobColor,0.05);
g.moveTo(cps[0].x,cps[0].y)
for(var iP:Number=1; iP<cps.length; iP++)
{
g.lineTo(cps[iP].x,cps[iP].y)
}
g.lineTo(cps[0].x,cps[0].y)

var sf:Number=0.25;
g.beginFill(0xFF0000,0.05);
g.moveTo(sf*cps[0].x,sf*cps[0].y)
for(iP=1; iP<cps.length; iP++)
{
g.lineTo(sf*cps[iP].x,sf*cps[iP].y)
}
g.lineTo(sf*cps[0].x,sf*cps[0].y)
}
private function mag(a:Number,b:Number):Number { return Math.sqrt(a*a+b*b); }
}
}

Please send me a link if you are able to use this in some interesting way. And don't forget to try the game: amoeba vs. bacteria,