Understanding Box2D’s one-way platforms, aka CLOUDS

One of the new features introduced with Box2D 2.1a is the improved contact listener class which comes in hand when we want to create one-way platforms, or “clouds”.

This can be made thanks to a function called before the contact is processed… something like “hey, two bodies are about to collide, what should I do?”… so you can decide to disable the contact for every collision you want.

The function used to do this task is PreSolve, working for all awake bodies that aren’t sensors.

If you don’t know what is a Box2D sensor, check Box2D Flash game creation tutorial – part 2.

So the concept is: listen for collisions, if a collision involves the cloud wall and the player, then check if the player is higher or lower than the cloud. If it’s lower, don’t process the collision and let the player fly through the cloud.

Let’s see the script, directly taken from Box2D Flash game creation tutorial – part 2:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	public class ball02 extends Sprite {
		// world creation
		public var world:b2World=new b2World(new b2Vec2(0,10.0),true);
		public var world_scale:int=30;
		// the player
		public var player:b2Body;
		// force to apply to the player
		public var force:b2Vec2;
		// variables to store whether the keys are pressed or not
		// true = pressed;
		// false = unpressed
		public var left,right,up,down:Boolean=false;
		// declaring my custom contact listener class
		public var contact_listener=new custom_contact_listener();
		public function ball02():void {
			// assigning the contact listener to the world
			world.SetContactListener(contact_listener);
			// calling debug draw function
			debug_draw();
			// drawing the boundaries
			draw_box(250,400,500,10,false,"ground");
			draw_box(0,200,10,400,false,"left");
			draw_box(500,200,10,400,false,"right");
			draw_box(250,0,500,10,false,"roof");
			draw_box(250,200,300,10,false,"middle");
			// adding the player at 250,200
			add_player(250,350);
			// adding some coins
			for (var i:int = 1; i<=5; i++) {
				draw_coin(Math.random()*400+50,Math.random()*150+25,Math.random()*3+2);
			}
			// listeners needed for the game to work
			addEventListener(Event.ENTER_FRAME, update);
			stage.addEventListener(KeyboardEvent.KEY_DOWN,on_key_down);
			stage.addEventListener(KeyboardEvent.KEY_UP,on_key_up);
		}
		// according to the key pressed, set the proper variable to "true"
		public function on_key_down(e:KeyboardEvent):void {
			switch (e.keyCode) {
				case 37 :
					left=true;
					break;
				case 38 :
					up=true;
					break;
				case 39 :
					right=true;
					break;
				case 40 :
					down=true;
					break;
			}
		}
		// according to the key released, set the proper variable to "false"
		public function on_key_up(e:KeyboardEvent):void {
			switch (e.keyCode) {
				case 37 :
					left=false;
					break;
				case 38 :
					up=false;
					break;
				case 39 :
					right=false;
					break;
				case 40 :
					down=false;
					break;
			}
		}
		// function to draw a coin
		public function draw_coin(px,py,r):void {
			var my_body:b2BodyDef= new b2BodyDef();
			my_body.position.Set(px/world_scale, py/world_scale);
			var my_circle:b2CircleShape=new b2CircleShape(r/world_scale);
			var my_fixture:b2FixtureDef = new b2FixtureDef();
			my_fixture.shape=my_circle;
			// look! it's a sensor!!
			my_fixture.isSensor=true;
			var world_body:b2Body=world.CreateBody(my_body);
			world_body.CreateFixture(my_fixture);
		}
		// simple function to draw a box
		public function draw_box(px,py,w,h,d,ud):void {
			var my_body:b2BodyDef= new b2BodyDef();
			my_body.position.Set(px/world_scale, py/world_scale);
			if (d) {
				my_body.type=b2Body.b2_dynamicBody;
			}
			var my_box:b2PolygonShape = new b2PolygonShape();
			my_box.SetAsBox(w/2/world_scale, h/2/world_scale);
			var my_fixture:b2FixtureDef = new b2FixtureDef();
			my_fixture.shape=my_box;
			var world_body:b2Body=world.CreateBody(my_body);
			world_body.SetUserData(ud);
			world_body.CreateFixture(my_fixture);
		}
		// function to add the player
		public function add_player(px,py):void {
			var my_body:b2BodyDef= new b2BodyDef();
			my_body.position.Set(px/world_scale, py/world_scale);
			my_body.type=b2Body.b2_dynamicBody;
			var my_circle:b2CircleShape=new b2CircleShape(10/world_scale);
			var my_fixture:b2FixtureDef = new b2FixtureDef();
			my_fixture.shape=my_circle;
			player=world.CreateBody(my_body);
			player.SetUserData("player");
			player.CreateFixture(my_fixture);
		}
		// debug draw
		public function debug_draw():void {
			var debug_draw:b2DebugDraw = new b2DebugDraw();
			var debug_sprite:Sprite = new Sprite();
			addChild(debug_sprite);
			debug_draw.SetSprite(debug_sprite);
			debug_draw.SetDrawScale(world_scale);
			debug_draw.SetFlags(b2DebugDraw.e_shapeBit);
			world.SetDebugDraw(debug_draw);
		}
		// function to be executed at every frame
		public function update(e:Event):void {
			// setting the force to null
			force=new b2Vec2(0,0);
			// according to the key(s) pressed, add the proper vector force
			if (left) {
				force.Add(new b2Vec2(-10,0));
			}
			if (right) {
				force.Add(new b2Vec2(10,0));
			}
			if (up) {
				force.Add(new b2Vec2(0,-20));
			}
			if (down) {
				force.Add(new b2Vec2(0,5));
			}
			// if there is any force, then apply it
			if (force.x||force.y) {
				player.ApplyForce(force,player.GetWorldCenter());
			}
			world.Step(1/30,10,10);
			world.ClearForces();
			// scanning through all bodies
			for (var worldbody:b2Body = world.GetBodyList(); worldbody; worldbody = worldbody.GetNext()) {
				// if a body is marked as "remove"...
				if (worldbody.GetUserData()=="remove") {
					// ... just remove it!!
					world.DestroyBody(worldbody);
				}
			}
			world.DrawDebugData();
		}
	}
}

Line 34: with the old function introduced at Understanding Box2D applicable forces I create a box marked as "middle".

No other changes on the main file, now let's see the custom_contact_listener.as file

package {
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Dynamics.Joints.*;
	import Box2D.Dynamics.Contacts.*;
	import Box2D.Common.*;
	import Box2D.Common.Math.*;
	class custom_contact_listener extends b2ContactListener {
		override public function BeginContact(contact:b2Contact):void {
			// getting the fixtures that collided
			var fixtureA:b2Fixture=contact.GetFixtureA();
			var fixtureB:b2Fixture=contact.GetFixtureB();
			// if the fixture is a sensor, mark the parent body to be removed
			if (fixtureB.IsSensor()) {
				fixtureB.GetBody().SetUserData("remove");
			}
			if (fixtureA.IsSensor()) {
				fixtureA.GetBody().SetUserData("remove");
			}
		}
		override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void {
			// getting the fixtures that collided
			var fixtureA:b2Fixture=contact.GetFixtureA();
			var fixtureB:b2Fixture=contact.GetFixtureB();
			// variable to handle bodies y position
			var player_y_position:Number;
			var platform_y_position:Number;
			// checking if the collision bodies are the ones marked as "middle" and "player"
			if ((fixtureA.GetBody().GetUserData()=="middle" && fixtureB.GetBody().GetUserData()=="player")||(fixtureA.GetBody().GetUserData()=="player" && fixtureB.GetBody().GetUserData()=="middle")) {
				// determining if the fixtureA represents the platform ("middle") or the player
				switch (fixtureA.GetBody().GetUserData()) {
					case "middle" :
						// determining y positions
						player_y_position=fixtureB.GetBody().GetPosition().y*30;
						platform_y_position=fixtureA.GetBody().GetPosition().y*30;
						break;
					case "player" :
						// determining y positions
						player_y_position=fixtureA.GetBody().GetPosition().y*30;
						platform_y_position=fixtureB.GetBody().GetPosition().y*30;
						break;
				}
				// checking distance between bodies
				var distance = player_y_position-platform_y_position;
				// if the distance is greater than player radius + half of the platform height...
				if (distance>-14.5) {
					// don't manage the contact
					contact.SetEnabled(false);
				}
			}
		}
	}
}

Line 22: beginning of the PreSolve function, the core of this example

Lines 24-25: getting the fixtures that generated the contact

Lines 27-28: declaring two variables to store y position of both bodies

Line 30: here I am checking if the fixtures are the one associated to the player and the one associated to the cloud

Lines 32-43: according to the fixture associated to the player and the one associated to the cloud, I am saving in the variables declared at lines 27-28 the y position of both bodies. I am multiplying directly by 30 without passing the right world_scale value declared at line 13 of the main class because it's not the purpose of this tutorial.

Line 45: Determining the vertical distance from the player and the cloud

Lines 47-50: If the player is not at least 14.5 pixels higher than the cloud, then disable the contact. Why 14.5? It's the sum of the ball radius (10) and half the cloud height (5)... and I am not using 15 because I found sometimes the distance when the ball falls on the cloud is 14.93, so the cloud won't "hold" the ball, letting it fall down. With 14.5, I am sure this won't happen.

And this is the result...

Move the ball by tapping arrow keys and watch the static object in the center of the stage act as a cloud... you can fly through it from bottom to top, but you can't do it from top to bottom.

Download the source code.

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
// 2048
// Dots
// Maze
// Pool
// Poux
// Pudi
// qomp
// Turn
// Zhed