Step by step creation of a Box2D soft body blob

As you should know, Box2D only manages rigid bodies, this means it assumes its bodies are made of the hardest material of the world, which cannot be bent or deformed in any way.

This saves a lot of CPU when handling the simulation but what if we need a soft body, such as a blob?

Around the web you can find some interesting examples based on springs taken from Boris the Brave’s controller pack but I’ve found them (and the author says the are) a bit glitchy.

So here we go with a step by step creation of a soft body blob only using distance joints.

1) Creating the bodies

The first step consists in creating the collection of bodies which will represent the soft body. There is a main body, and a number of satellite bodies, as you can see from this script:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.Joints.*;
	public class Main extends Sprite {
		private var world:b2World;
		private var worldScale:Number=30;
		private var sphereVector:Vector.;
		private var blobX:Number=320;
		private var blobY:Number=240;
		private var particleNumber:Number=16;
		private var particleDistance:Number=50;
		public function Main() {
			world=new b2World(new b2Vec2(0,10),true);
			debugDraw();
			floor();
			sphereVector=new Vector.();
			sphereVector.push(sphere(blobX,blobY,15));
			for (var i:Number=0; i

Pay particular attention to particleNumber variable which represents the number of satellite bodies which will build the blob itself, and to particleDistance which basically is the radius of the blob.

This is the result:

These are all the bodies we need to build the blob.

2) Binding satellites with the main body

Now I am going to create distance joints to bind satellites with the main body

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.Joints.*;
	public class Main extends Sprite {
		private var world:b2World;
		private var worldScale:Number=30;
		private var sphereVector:Vector.;
		private var blobX:Number=320;
		private var blobY:Number=240;
		private var particleNumber:Number=16;
		private var particleDistance:Number=50;
		public function Main() {
			world=new b2World(new b2Vec2(0,10),true);
			debugDraw();
			floor();
			sphereVector=new Vector.();
			sphereVector.push(sphere(blobX,blobY,15));
			for (var i:Number=0; i

At the end of this step, every satellite has its distance joint with the main body:

However, just using these joints is not enough, as I also need to create distance joints between satellites

3) Binding satellites one to each other

Now it's time to add some other joints to bind satellites one to each other, this way, so we can actually get some kind of wheel

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.Joints.*;
	public class Main extends Sprite {
		private var world:b2World;
		private var worldScale:Number=30;
		private var sphereVector:Vector.;
		private var blobX:Number=320;
		private var blobY:Number=240;
		private var particleNumber:Number=16;
		private var particleDistance:Number=50;
		public function Main() {
			world=new b2World(new b2Vec2(0,10),true);
			debugDraw();
			floor();
			sphereVector=new Vector.();
			sphereVector.push(sphere(blobX,blobY,15));
			for (var i:Number=0; i0) {
					var distanceX:Number=posX/worldScale-sphereVector[sphereVector.length-2].GetPosition().x;
					var distanceY:Number=posY/worldScale-sphereVector[sphereVector.length-2].GetPosition().y;
					var distance:Number=Math.sqrt(distanceX*distanceX+distanceY*distanceY);
					dJoint.bodyA=sphereVector[sphereVector.length-2];
					dJoint.bodyB=sphereVector[sphereVector.length-1];
					dJoint.localAnchorA=new b2Vec2(0,0);
					dJoint.localAnchorB=new b2Vec2(0,0);
					dJoint.length=distance;
					distanceJoint=world.CreateJoint(dJoint) as b2DistanceJoint;
				}
				if (i==particleNumber-1) {
					distanceX=posX/worldScale-sphereVector[1].GetPosition().x;
					distanceY=posY/worldScale-sphereVector[1].GetPosition().y;
					distance=Math.sqrt(distanceX*distanceX+distanceY*distanceY);
					dJoint.bodyA=sphereVector[1];
					dJoint.bodyB=sphereVector[sphereVector.length-1];
					dJoint.localAnchorA=new b2Vec2(0,0);
					dJoint.localAnchorB=new b2Vec2(0,0);
					dJoint.length=distance;
					distanceJoint=world.CreateJoint(dJoint) as b2DistanceJoint;
				}
			}
			addEventListener(Event.ENTER_FRAME,updateWorld);
		}
		private function sphere(pX:int,pY:int,r:Number):b2Body {
			var bodyDef:b2BodyDef=new b2BodyDef();
			bodyDef.position.Set(pX/worldScale,pY/worldScale);
			var circleShape:b2CircleShape;
			circleShape=new b2CircleShape(r/worldScale);
			var fixtureDef:b2FixtureDef=new b2FixtureDef();
			fixtureDef.shape=circleShape;
			fixtureDef.density=1;
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.5;
			var theSphere:b2Body=world.CreateBody(bodyDef);
			theSphere.CreateFixture(fixtureDef);
			return theSphere;
		}
		private function floor():void {
			var bodyDef:b2BodyDef=new b2BodyDef();
			bodyDef.position.Set(320/worldScale,465/worldScale);
			var polygonShape:b2PolygonShape=new b2PolygonShape();
			polygonShape.SetAsBox(320/worldScale,15/worldScale);
			var fixtureDef:b2FixtureDef=new b2FixtureDef();
			fixtureDef.shape=polygonShape;
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.5;
			var theFloor:b2Body=world.CreateBody(bodyDef);
			theFloor.CreateFixture(fixtureDef);
		}
		private function debugDraw():void {
			var debugDraw:b2DebugDraw=new b2DebugDraw();
			var debugSprite:Sprite=new Sprite();
			addChild(debugSprite);
			debugDraw.SetSprite(debugSprite);
			debugDraw.SetDrawScale(worldScale);
			debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
			debugDraw.SetFillAlpha(0.5);
			world.SetDebugDraw(debugDraw);
		}
		private function updateWorld(e:Event):void {
			world.Step(1/30,10,10);
			world.ClearForces();
			world.DrawDebugData();
		}
	}
}

And now the blob is ready to be seen in action:

Nothing happens because all bodies are still static

4) Making bodies dynamic

It's time to make bodies dynamic and see what happens:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.Joints.*;
	public class Main extends Sprite {
		private var world:b2World;
		private var worldScale:Number=30;
		private var sphereVector:Vector.;
		private var blobX:Number=320;
		private var blobY:Number=240;
		private var particleNumber:Number=16;
		private var particleDistance:Number=50;
		public function Main() {
			world=new b2World(new b2Vec2(0,10),true);
			debugDraw();
			floor();
			sphereVector=new Vector.();
			sphereVector.push(sphere(blobX,blobY,15));
			for (var i:Number=0; i0) {
					var distanceX:Number=posX/worldScale-sphereVector[sphereVector.length-2].GetPosition().x;
					var distanceY:Number=posY/worldScale-sphereVector[sphereVector.length-2].GetPosition().y;
					var distance:Number=Math.sqrt(distanceX*distanceX+distanceY*distanceY);
					dJoint.bodyA=sphereVector[sphereVector.length-2];
					dJoint.bodyB=sphereVector[sphereVector.length-1];
					dJoint.localAnchorA=new b2Vec2(0,0);
					dJoint.localAnchorB=new b2Vec2(0,0);
					dJoint.length=distance;
					distanceJoint=world.CreateJoint(dJoint) as b2DistanceJoint;
				}
				if (i==particleNumber-1) {
					distanceX=posX/worldScale-sphereVector[1].GetPosition().x;
					distanceY=posY/worldScale-sphereVector[1].GetPosition().y;
					distance=Math.sqrt(distanceX*distanceX+distanceY*distanceY);
					dJoint.bodyA=sphereVector[1];
					dJoint.bodyB=sphereVector[sphereVector.length-1];
					dJoint.localAnchorA=new b2Vec2(0,0);
					dJoint.localAnchorB=new b2Vec2(0,0);
					dJoint.length=distance;
					distanceJoint=world.CreateJoint(dJoint) as b2DistanceJoint;
				}
			}
			addEventListener(Event.ENTER_FRAME,updateWorld);
		}
		private function sphere(pX:int,pY:int,r:Number):b2Body {
			var bodyDef:b2BodyDef=new b2BodyDef();
			bodyDef.position.Set(pX/worldScale,pY/worldScale);
			bodyDef.type=b2Body.b2_dynamicBody;
			var circleShape:b2CircleShape;
			circleShape=new b2CircleShape(r/worldScale);
			var fixtureDef:b2FixtureDef=new b2FixtureDef();
			fixtureDef.shape=circleShape;
			fixtureDef.density=1;
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.5;
			var theSphere:b2Body=world.CreateBody(bodyDef);
			theSphere.CreateFixture(fixtureDef);
			return theSphere;
		}
		private function floor():void {
			var bodyDef:b2BodyDef=new b2BodyDef();
			bodyDef.position.Set(320/worldScale,465/worldScale);
			var polygonShape:b2PolygonShape=new b2PolygonShape();
			polygonShape.SetAsBox(320/worldScale,15/worldScale);
			var fixtureDef:b2FixtureDef=new b2FixtureDef();
			fixtureDef.shape=polygonShape;
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.5;
			var theFloor:b2Body=world.CreateBody(bodyDef);
			theFloor.CreateFixture(fixtureDef);
		}
		private function debugDraw():void {
			var debugDraw:b2DebugDraw=new b2DebugDraw();
			var debugSprite:Sprite=new Sprite();
			addChild(debugSprite);
			debugDraw.SetSprite(debugSprite);
			debugDraw.SetDrawScale(worldScale);
			debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
			debugDraw.SetFillAlpha(0.5);
			world.SetDebugDraw(debugDraw);
		}
		private function updateWorld(e:Event):void {
			world.Step(1/30,10,10);
			world.ClearForces();
			world.DrawDebugData();
		}
	}
}

Ready to see the blob? here it is:

Unfortunately the blob is really rigid, because distance joints do not allow bodies to change the distance among them (that's why it's called "distance" joint).

5) Playing with damping and frequency

Too good we can play with distance joint damping and frequency to turn boring distance joints into happy springs:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.Joints.*;
	public class Main extends Sprite {
		private var world:b2World;
		private var worldScale:Number=30;
		private var sphereVector:Vector.;
		private var blobX:Number=320;
		private var blobY:Number=240;
		private var particleNumber:Number=16;
		private var particleDistance:Number=50;
		public function Main() {
			world=new b2World(new b2Vec2(0,10),true);
			debugDraw();
			floor();
			sphereVector=new Vector.();
			sphereVector.push(sphere(blobX,blobY,15));
			for (var i:Number=0; i0) {
					var distanceX:Number=posX/worldScale-sphereVector[sphereVector.length-2].GetPosition().x;
					var distanceY:Number=posY/worldScale-sphereVector[sphereVector.length-2].GetPosition().y;
					var distance:Number=Math.sqrt(distanceX*distanceX+distanceY*distanceY);
					dJoint.bodyA=sphereVector[sphereVector.length-2];
					dJoint.bodyB=sphereVector[sphereVector.length-1];
					dJoint.localAnchorA=new b2Vec2(0,0);
					dJoint.localAnchorB=new b2Vec2(0,0);
					dJoint.length=distance;
					distanceJoint=world.CreateJoint(dJoint) as b2DistanceJoint;
				}
				if (i==particleNumber-1) {
					distanceX=posX/worldScale-sphereVector[1].GetPosition().x;
					distanceY=posY/worldScale-sphereVector[1].GetPosition().y;
					distance=Math.sqrt(distanceX*distanceX+distanceY*distanceY);
					dJoint.bodyA=sphereVector[1];
					dJoint.bodyB=sphereVector[sphereVector.length-1];
					dJoint.localAnchorA=new b2Vec2(0,0);
					dJoint.localAnchorB=new b2Vec2(0,0);
					dJoint.length=distance;
					distanceJoint=world.CreateJoint(dJoint) as b2DistanceJoint;
				}
			}
			addEventListener(Event.ENTER_FRAME,updateWorld);
		}
		private function sphere(pX:int,pY:int,r:Number):b2Body {
			var bodyDef:b2BodyDef=new b2BodyDef();
			bodyDef.position.Set(pX/worldScale,pY/worldScale);
			bodyDef.type=b2Body.b2_dynamicBody;
			var circleShape:b2CircleShape;
			circleShape=new b2CircleShape(r/worldScale);
			var fixtureDef:b2FixtureDef=new b2FixtureDef();
			fixtureDef.shape=circleShape;
			fixtureDef.density=1;
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.5;
			var theSphere:b2Body=world.CreateBody(bodyDef);
			theSphere.CreateFixture(fixtureDef);
			return theSphere;
		}
		private function floor():void {
			var bodyDef:b2BodyDef=new b2BodyDef();
			bodyDef.position.Set(320/worldScale,465/worldScale);
			var polygonShape:b2PolygonShape=new b2PolygonShape();
			polygonShape.SetAsBox(320/worldScale,15/worldScale);
			var fixtureDef:b2FixtureDef=new b2FixtureDef();
			fixtureDef.shape=polygonShape;
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.5;
			var theFloor:b2Body=world.CreateBody(bodyDef);
			theFloor.CreateFixture(fixtureDef);
		}
		private function debugDraw():void {
			var debugDraw:b2DebugDraw=new b2DebugDraw();
			var debugSprite:Sprite=new Sprite();
			addChild(debugSprite);
			debugDraw.SetSprite(debugSprite);
			debugDraw.SetDrawScale(worldScale);
			debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
			debugDraw.SetFillAlpha(0.5);
			world.SetDebugDraw(debugDraw);
		}
		private function updateWorld(e:Event):void {
			world.Step(1/30,10,10);
			world.ClearForces();
			world.DrawDebugData();
		}
	}
}

And finally here it is:

Now the distance joints are not that rigid, and playing with dampingRatio and frequencyHz properties (recommended range respectively 0 to 1 and 1 to 30) you can achieve a good blob effect.

I am making a game out of it, stay tuned.

Download the source code.

Last minute edit: my Box2D book is expected to be on the shelves on October 21.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers