Managing multiple gravities with Box2D

One of the apparently hardest things to do with Box2D is assigning different gravity forces to different objects.

For instance, you may want the world to be ruled with regular gravity, while you don’t want the bullet fired by your character to be affected, or you may want regular gravity for all boxes and inverted one for all circles, like in the example I am talking about.

There is one way to simulate different gravity and one way to apply different gravity in your Box2D projects.

This is the main script, directly from the HelloWorld.as example provided with Box2D distribution (so just copy/paste the code)

In some case you should need to refresh the page to see the movies in action.

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	public class HelloWorld extends Sprite {
		public var m_world:b2World;
		public var m_iterations:int=10;
		public var m_timeStep:Number=1.0/30.0;
		public function HelloWorld() {
			addEventListener(Event.ENTER_FRAME, Update, false, 0, true);
			var worldAABB:b2AABB = new b2AABB();
			worldAABB.lowerBound.Set(-100.0, -100.0);
			worldAABB.upperBound.Set(100.0, 100.0);
			var gravity:b2Vec2=new b2Vec2(0.0,10.0);
			var doSleep:Boolean=true;
			m_world=new b2World(worldAABB,gravity,doSleep);
			var dbgDraw:b2DebugDraw = new b2DebugDraw();
			var dbgSprite:Sprite = new Sprite();
			addChild(dbgSprite);
			dbgDraw.m_sprite=dbgSprite;
			dbgDraw.m_drawScale=30.0;
			dbgDraw.m_fillAlpha=0.5;
			dbgDraw.m_lineThickness=1.0;
			dbgDraw.m_drawFlags=b2DebugDraw.e_shapeBit;
			m_world.SetDebugDraw(dbgDraw);
			var body:b2Body;
			var bodyDef:b2BodyDef;
			var boxDef:b2PolygonDef;
			var circleDef:b2CircleDef;
			bodyDef = new b2BodyDef();
			bodyDef.position.Set(10, 12);
			boxDef = new b2PolygonDef();
			boxDef.SetAsBox(30, 0.5);
			boxDef.friction=0.3;
			boxDef.density=0;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(boxDef);
			body.SetMassFromShapes();
			bodyDef = new b2BodyDef();
			bodyDef.position.Set(10, 0);
			boxDef = new b2PolygonDef();
			boxDef.SetAsBox(30, 0.5);
			boxDef.friction=0.3;
			boxDef.density=0;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(boxDef);
			body.SetMassFromShapes();
			for (var i:int = 1; i < 10; i++) {
				bodyDef = new b2BodyDef();
				bodyDef.position.x=Math.random()*12+2;
				bodyDef.position.y=Math.random()+5;
				var rX:Number=Math.random()+0.2;
				var rY:Number=Math.random()+0.2;
				if (Math.random()<0.5) {
					boxDef = new b2PolygonDef();
					boxDef.SetAsBox(rX, rY);
					boxDef.density=1.0;
					boxDef.friction=0.5;
					boxDef.restitution=0.2;
					bodyDef.userData = new Sprite();
					bodyDef.userData.name="box";
					body=m_world.CreateBody(bodyDef);
					body.CreateShape(boxDef);
				} else {
					circleDef = new b2CircleDef();
					circleDef.radius=rX;
					circleDef.density=1.0;
					circleDef.friction=0.5;
					circleDef.restitution=0.2;
					bodyDef.userData = new Sprite();
					bodyDef.userData.name="circle";
					body=m_world.CreateBody(bodyDef);
					body.CreateShape(circleDef);
				}
				body.SetMassFromShapes();
				addChild(bodyDef.userData);
			}
		}
		public function Update(e:Event):void {
			m_world.Step(m_timeStep, m_iterations);
		}
	}
}

The only interesting lines are line 17 where I declare the gravity vector and lines 64 and 74 where I assign names to the objects... box for boxes and circle for circles

And this is the result:

As you can see, the world is ruled by standard gravity, but as I said I want boxes to be ruled by normal gravity and circles to be ruled by inverted gravity

Now, let's see the first method, called (by myself)....

The antagonist forces method

As the name means, the principle is applying an antagonist force to circles in order to simulate a reverse gravity. Since the default gravity is (0,10), I am going to apply a (0,-20) force to all circles.

This is the updated Update function:

public function Update(e:Event):void {
	var ant_gravity = b2Vec2;
	m_world.Step(m_timeStep, m_iterations);
	for (var bb:b2Body = m_world.m_bodyList; bb; bb = bb.m_next) {
		if (bb.GetUserData()!=null) {
			if (bb.GetUserData().name=="circle") {
				ant_gravity = new b2Vec2(0.0,-20.0*bb.GetMass());
				bb.ApplyForce(ant_gravity,bb.GetWorldCenter());
			}
		}
	}
}

And this is the result:

The result is what we want, anyway now you may say to create objects that aren't affected by gravity you just have to change line 88 with

ant_gravity = new b2Vec2(0.0,-10.0*bb.GetMass());

but look at the result...

circles are slowly falling down. This happens because when you call Step functions, bodies have an acceleration due to gravity, and the opposite force isn't enough to nullify it.

Let's see the second method, called

The island method

This method is named after the filename of the library you have to edit... it's b2Island.as inside Dynamics folder.

First, the only line inside Update function must be once again

m_world.Step(m_timeStep, m_iterations);

as in the first example.

Then, this is the modified version of Solve function inside b2Island.as file:

public function Solve(step:b2TimeStep, gravity:b2Vec2, correctPositions:Boolean, allowSleep:Boolean) : void
{
	var i:int;
	var b:b2Body;
	var joint:b2Joint;
	var applied_gravity:b2Vec2
	
	// Integrate velocities and apply damping.
	for (i = 0; i < m_bodyCount; ++i)
	{
		b = m_bodies[i];
		
		if (b.IsStatic())
			continue;
		
		// Integrate velocities.
		//b.m_linearVelocity += step.dt * (gravity + b.m_invMass * b.m_force);
		if(b.m_userData.name=="circle"){
			applied_gravity = new b2Vec2(-gravity.x,-gravity.y)
		}
		else{
			applied_gravity = gravity;
		}
		b.m_linearVelocity.x += step.dt * (applied_gravity.x + b.m_invMass * b.m_force.x);
		b.m_linearVelocity.y += step.dt * (applied_gravity.y + b.m_invMass * b.m_force.y);
		b.m_angularVelocity += step.dt * b.m_invI * b.m_torque;
		
		// Reset forces.
		b.m_force.SetZero();
		b.m_torque = 0.0;
		
		// Apply damping.
		// ODE: dv/dt + c * v = 0
		// Solution: v(t) = v0 * exp(-c * t)
		// Time step: v(t + dt) = v0 * exp(-c * (t + dt)) = v0 * exp(-c * t) * exp(-c * dt) = v * exp(-c * dt)
		// v2 = exp(-c * dt) * v1
		// Taylor expansion:
		// v2 = (1.0f - c * dt) * v1
		b.m_linearVelocity.Multiply( b2Math.b2Clamp(1.0 - step.dt * b.m_linearDamping, 0.0, 1.0) );
		b.m_angularVelocity *= b2Math.b2Clamp(1.0 - step.dt * b.m_angularDamping, 0.0, 1.0);
		
		// Check for large velocities.
		//if (b2Dot(b->m_linearVelocity, b->m_linearVelocity) > b2_maxLinearVelocitySquared)
		if ((b.m_linearVelocity.LengthSquared()) > b2Settings.b2_maxLinearVelocitySquared)
		{
			b.m_linearVelocity.Normalize();
			b.m_linearVelocity.x *= b2Settings.b2_maxLinearVelocity;
			b.m_linearVelocity.y *= b2Settings.b2_maxLinearVelocity;
		}
		
		if (b.m_angularVelocity * b.m_angularVelocity > b2Settings.b2_maxAngularVelocitySquared)
		{
			if (b.m_angularVelocity < 0.0)
			{
				b.m_angularVelocity = -b2Settings.b2_maxAngularVelocity;
			}
			else
			{
				b.m_angularVelocity = b2Settings.b2_maxAngularVelocity;
			}
		}
	}
	
	var contactSolver:b2ContactSolver = new b2ContactSolver(step, m_contacts, m_contactCount, m_allocator);
	
	// Initialize velocity constraints.
	contactSolver.InitVelocityConstraints(step);
	
	for (i = 0; i < m_jointCount; ++i)
	{
		joint = m_joints[i];
		joint.InitVelocityConstraints(step);
	}
	
	// Solve velocity constraints.
	for (i = 0; i < step.maxIterations; ++i)
	{
		contactSolver.SolveVelocityConstraints();
		
		for (var j:int = 0; j < m_jointCount; ++j)
		{
			joint = m_joints[j];
			joint.SolveVelocityConstraints(step);
		}
	}
	
	// Post-solve (store impulses for warm starting).
	contactSolver.FinalizeVelocityConstraints();
	
	// Integrate positions.
	for (i = 0; i < m_bodyCount; ++i)
	{
		b = m_bodies[i];
		
		if (b.IsStatic())
			continue;
		
		// Store positions for continuous collision.
		b.m_sweep.c0.SetV(b.m_sweep.c);
		b.m_sweep.a0 = b.m_sweep.a;
		
		// Integrate
		//b.m_sweep.c += step.dt * b.m_linearVelocity;
		b.m_sweep.c.x += step.dt * b.m_linearVelocity.x;
		b.m_sweep.c.y += step.dt * b.m_linearVelocity.y;
		b.m_sweep.a += step.dt * b.m_angularVelocity;
		
		// Compute new transform
		b.SynchronizeTransform();
		
		// Note: shapes are synchronized later.
	}
	
	if (correctPositions)
	{
		// Initialize position constraints.
		// Contacts don't need initialization.
		for (i = 0; i < m_jointCount; ++i)
		{
			joint = m_joints[i];
			joint.InitPositionConstraints();
		}
		
		// Iterate over constraints.
		for (m_positionIterationCount = 0; m_positionIterationCount < step.maxIterations; ++m_positionIterationCount)
		{
			var contactsOkay:Boolean = contactSolver.SolvePositionConstraints(b2Settings.b2_contactBaumgarte);
			
			var jointsOkay:Boolean = true;
			for (i = 0; i < m_jointCount; ++i)
			{
				joint = m_joints[i];
				var jointOkay:Boolean = joint.SolvePositionConstraints();
				jointsOkay = jointsOkay && jointOkay;
			}
			
			if (contactsOkay && jointsOkay)
			{
				break;
			}
		}
	}
	
	Report(contactSolver.m_constraints);
	
	if (allowSleep){
		
		var minSleepTime:Number = Number.MAX_VALUE;
		
		var linTolSqr:Number = b2Settings.b2_linearSleepTolerance * b2Settings.b2_linearSleepTolerance;
		var angTolSqr:Number = b2Settings.b2_angularSleepTolerance * b2Settings.b2_angularSleepTolerance;
		
		for (i = 0; i < m_bodyCount; ++i)
		{
			b = m_bodies[i];
			if (b.m_invMass == 0.0)
			{
				continue;
			}
			
			if ((b.m_flags & b2Body.e_allowSleepFlag) == 0)
			{
				b.m_sleepTime = 0.0;
				minSleepTime = 0.0;
			}
			
			if ((b.m_flags & b2Body.e_allowSleepFlag) == 0 ||
				b.m_angularVelocity * b.m_angularVelocity > angTolSqr ||
				b2Math.b2Dot(b.m_linearVelocity, b.m_linearVelocity) > linTolSqr)
			{
				b.m_sleepTime = 0.0;
				minSleepTime = 0.0;
			}
			else
			{
				b.m_sleepTime += step.dt;
				minSleepTime = b2Math.b2Min(minSleepTime, b.m_sleepTime);
			}
		}
		
		if (minSleepTime >= b2Settings.b2_timeToSleep)
		{
			for (i = 0; i < m_bodyCount; ++i)
			{
				b = m_bodies[i];
				b.m_flags |= b2Body.e_sleepFlag;
				b.m_linearVelocity.SetZero();
				b.m_angularVelocity = 0.0;
			}
		}
	}
}

Let's see the lines I added/changed:

Line 162: declaring a new vector that will handle the applied gravity

Line 174: if the body we are solving is called "circle"...

Line 175: set applied_gravity vector as the opposite of the default gravity one

Lines 177-179: If not, set applied_gravity vector to default gravity one

Lines 180-181: Use applied_gravity vector instead of gravity vector to determine body's linear velocity

And this is the result:

That is working perfectly even if you change line 175 this way:

applied_gravity = new b2Vec2(0,0)

To have no gravity circles, so this last method is preferred.

Let me think what do you think about it.

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

215 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
// Stairs
// 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