Talking about Actionscript 3, Box2D and Flash.
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.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.