Saturday, December 22, 2012

Box2D in Flash and AS3.. explained for beginners Part 2

For part one click here.
See the demo from here.

In this Part we will update the world, create some walls and apply impulse on the player when the mouse is clicked, so lets get started

Make things come alive!

We will do that by updating the box2D world every frame, first add an ENTER_FRAME event listener and create the handler method like this
public function init(e:Event = null):void
{    
    createWorld();
    
    player = createPlayer(40, 50, 50, 10);
        
    // When Clicking on stage
    stage.addEventListener(MouseEvent.CLICK, stageClicked);
    // Enter frame listener
    addEventListener(Event.ENTER_FRAME, update);
}
    
public function update(e:Event):void
{
    // This method will be called every frame and here where we will update 
    // the box2D world
}
Good, now to update the box2d world we need to call a method called Step on the world object
public function update(e:Event):void
{
    world.Step(1 / 30, 10, 10);
}
This method takes three parameters ( time step, velocity iterations , position iterations ) :
  1. Time step: we set it here to 1 / 30, which means every frame the world will step only 1 / 30 seconds, this will give a fairly good simulation and realistic collisions, decreasing this will increase the accuracy of the calculations of collisions but also will decrease the speed of the animation.
  2. Velocity iterations and position iterations: usually we set it to 10. You can set it to lower or higher. The consequences of setting it lower is less fine detail in the simulation, but a possible speed increase. The opposite is true of setting them higher.
If you tested the project now, nothing would change, why? Because the sprites position will not be updated with the box2d object, so we have to update all the displays referenced by the box2d objects in the enter frame handler.
But before doing that, lets first know how we can iterate through all bodies in a b2World
for (var body:b2Body = world.GetBodyList(); body != null; body = body.GetNext())
how is that possible? We all know that for statement requires 3 parts
for ( Initialization ; Condition; Afterthough )
We start off by getting the first body in the list using the GetBodyList method, we will keep looping as long as the body is not equal to null, and every iteration we will go to the next body in the list, see the figure below
Back to updating the sprites referenced by the box2d objects, what we are going to do is update the position and rotation of the sprites in the userData of each box2d body.
public function update(e:Event):void
{
    world.Step(1 / 30, 10, 10);
    var temp:Sprite;
    for (var body:b2Body = world.GetBodyList(); body != null; body = body.GetNext())
    {
        if (body.GetUserData())
        {
            temp = body.GetUserData() as Sprite;
            temp.x = body.GetPosition().x * PM;
            temp.y = body.GetPosition().y * PM;
            temp.rotation = body.GetAngle() * (180 / Math.PI); // radians to degrees
        }
    }
}
We looped through all box2d bodies then we checked if GetUserData() is not null which means there is a sprite referenced by this body, then we saved this sprite in a var temp of type Sprite ( don't forget to cast to Sprite ), we then updated the position of this sprite after converting to pixels by multiplying with PM because sprites work with pixels not meters unlike box2d objects. One last thing we updated the rotation of the sprite, Note that GetAngle method returns radians so we have to convert it first to degrees.
That is it! You should now have a falling box ( player ).

Creating walls

The same way as we did before when we were creating the player except we will make it static instead of dynamic
public function createWall(_x:Number, _y:Number, _width:Number, _height:Number):b2Body
{    
    _x = _x + _width / 2;
    _y = _y + _height / 2;
    
    // Create Wall Sprite Using code
    var wallSprite:Sprite = new Sprite();
    wallSprite.graphics.beginFill(0xe7d7c0, 1);
    wallSprite.graphics.drawRect( -_width / 2, -_height / 2 , _width, _height);
    wallSprite.graphics.endFill();
    wallSprite.x = _x;
    wallSprite.y = _y;
    addChild(wallSprite);
    
    // Create body definition
    var bodyDef:b2BodyDef = new b2BodyDef();
    bodyDef.userData = wallSprite;
    bodyDef.type     = b2Body.b2_staticBody;
    bodyDef.position.Set( _x / PM, _y / PM);
    
    // Create body from world using bodyDef
    var body:b2Body = world.CreateBody(bodyDef);
    
    // Create shape
    var shape:b2PolygonShape = new b2PolygonShape();
    shape.SetAsBox((_width / 2) / PM, (_height / 2) / PM);
    
    // Create fixtureDef giving shape
    var fixtureDef:b2FixtureDef = new b2FixtureDef();
    fixtureDef.shape       = shape;
    fixtureDef.restitution = 0.2;
    fixtureDef.friction    = 1;
    fixtureDef.density     = 0.5;
    
    // Pass the fixtureDef to the createFixture method in the body object
    body.CreateFixture(fixtureDef);
    
    return body;
}

The first two lines in the method we added half the width and half the height to x and y respectively, why? As we said in part1 of this tutorial that the origin is in the center of the object, and that's fine for creating the player but when creating walls it would be a lot easier if the origin was at the top left not the center so we just take the supplied x and y ( that are supposed to be the top left coordinates ) and add them to half width and half height to get the center coordinates.
Note also that we set the type to static this time.

Now let us create some walls in the init() method
var th:uint = 10;
// Vertical walls
createWall(0, 0                    , th, stage.stageHeight);
createWall(stage.stageWidth - th, 0, th, stage.stageHeight);
// Horizontal walls
createWall(0, 0                     , stage.stageWidth, th);
createWall(0, stage.stageHeight - th, stage.stageWidth, th);
You should now see some walls and a player.

Adding mouse listener and applying Impulse to the player

We already added a mouse event listener to the stage in the init method, now create the MouseEvent handler and add this lines
public function stageClicked(e:MouseEvent):void
{
    var impulse_x:Number = (mouseX / PM - player.GetPosition().x);
    var impulse_y:Number = (mouseY / PM - player.GetPosition().y);
    
    var impulse:b2Vec2 = new b2Vec2(impulse_x, impulse_y);
    impulse.Multiply( 2 / 3 );
    player.ApplyImpulse(impulse, player.GetPosition());
}
lets go through this code.
We want to apply an impulse on the player, this impulse should increase when the mouse goes farther away from the player. So impulse_x and impulse_y calculate the distance between the mouse and the player in meters, that's why we divided mouseX and mouseY by PM.
Next we created a b2Vec2 from the impulse_x and impulse_y then we muliplied it by 2 / 3, why? It just seemed more realistic but you can escape this step, anyway this is a very useful method (remember it).
Finally we applied an impulse on the player using the impulse and player position ( the position where the impulse should be applied ).

Part 2: All come together

public class Main extends MovieClip
{
    
    public static var world:b2World;
    public static const PM:uint = 30;
    private var player:b2Body;
    
    public function Main() 
    {
        if (stage) init();
        else addEventListener(Event.ADDED_TO_STAGE, init);
    }
    
    public function init(e:Event = null):void
    {  
        
        createWorld();
        
        player = createPlayer(40, 50, 50, 10);
        
        addEventListener(Event.ENTER_FRAME, update);
        stage.addEventListener(MouseEvent.CLICK, stageClicked);
        
        var th:uint = 10;
        // Vertical walls
        createWall(0, 0                    , th, stage.stageHeight);
        createWall(stage.stageWidth - th, 0, th, stage.stageHeight);
        // Horizontal walls
        createWall(0, 0                     , stage.stageWidth, th);
        createWall(0, stage.stageHeight - th, stage.stageWidth, th);
        
    }
    
    public function stageClicked(e:MouseEvent):void
    {
        var impulse_x:Number = (mouseX / PM - player.GetPosition().x);
        var impulse_y:Number = (mouseY / PM - player.GetPosition().y);
        
        var impulse:b2Vec2 = new b2Vec2(impulse_x, impulse_y);
        impulse.Multiply( 2 / 3 );
        player.ApplyImpulse(impulse, player.GetPosition());
    }
    
    public function update(e:Event):void
    {
        world.Step(1 / 30, 10, 10);
        var temp:Sprite;
        for (var body:b2Body = world.GetBodyList(); body != null; body = body.GetNext())
        {
            if (body.GetUserData())
            {
                temp = body.GetUserData() as Sprite;
                temp.x = body.GetPosition().x * PM;
                temp.y = body.GetPosition().y * PM;
                temp.rotation = body.GetAngle() * (180 / Math.PI); // radians to degrees
            }
        }
    }
    
    public function createWorld():void
    {
        var gravity:b2Vec2 = new b2Vec2(0, 9.8);
        var sleep:Boolean = true;
        
        world = new b2World(gravity, sleep);
    }
    
    public function createWall(_x:Number, _y:Number, _width:Number, _height:Number):b2Body
    {    
        _x = _x + _width / 2;
        _y = _y + _height / 2;
        
        // Create Wall Sprite Using code
        var wallSprite:Sprite = new Sprite();
        wallSprite.graphics.beginFill(0xe7d7c0, 1);
        wallSprite.graphics.drawRect( -_width / 2, -_height / 2 , _width, _height);
        wallSprite.graphics.endFill();
        wallSprite.x = _x;
        wallSprite.y = _y;
        addChild(wallSprite);
        
        // Create body definition
        var bodyDef:b2BodyDef = new b2BodyDef();
        bodyDef.userData = wallSprite;
        bodyDef.type     = b2Body.b2_staticBody;
        bodyDef.position.Set( _x / PM, _y / PM);
        
        // Create body from world using bodyDef
        var body:b2Body = world.CreateBody(bodyDef);
        
        // Create shape
        var shape:b2PolygonShape = new b2PolygonShape();
        shape.SetAsBox((_width / 2) / PM, (_height / 2) / PM);
        
        // Create fixtureDef giving shape
        var fixtureDef:b2FixtureDef = new b2FixtureDef();
        fixtureDef.shape       = shape;
        fixtureDef.restitution = 0.2;
        fixtureDef.friction    = 1;
        fixtureDef.density     = 0.5;
        
        // Pass the fixtureDef to the createFixture method in the body object
        body.CreateFixture(fixtureDef);
        
        return body;
    }
    
    public function createPlayer(_x:Number , _y:Number, _width:Number, _height:Number):b2Body
    {
        // Create car sprite
        var carSprite:Sprite = new Sprite();
        carSprite.graphics.beginFill(0xafafaf, 1);
        carSprite.graphics.drawRect( -_width / 2, -_height / 2 , _width, _height);
        carSprite.graphics.endFill();
        carSprite.x = _x;
        carSprite.y = _y;
        addChild(carSprite);
        
        // Create body definition
        var bodyDef:b2BodyDef = new b2BodyDef();
        bodyDef.userData = carSprite;
        bodyDef.type     = b2Body.b2_dynamicBody;
        bodyDef.position.Set(_x / PM, _y / PM);
        
        // Create body from world using bodyDef
        var body:b2Body = world.CreateBody(bodyDef);
        
        // Create shape
        var shape:b2PolygonShape = new b2PolygonShape();
        shape.SetAsBox(_width / 2 / PM, _height / 2 / PM);
        
        // Create fixtureDef giving shape
        var fixtureDef:b2FixtureDef = new b2FixtureDef();
        fixtureDef.shape       = shape;
        fixtureDef.restitution = 0.2;
        fixtureDef.friction    = 1;
        fixtureDef.density     = 0.5;
        
        // Pass the fixtureDef to the createFixture method in the body object
        body.CreateFixture(fixtureDef);
        
        return body;
    }
    
}

No comments:

Post a Comment