Adding physics to your app

In this section, you will learn how to set up the physics calculations that run in the background of your app. The data we receive from these calculations allows your hero navigate and interact with the game level appropriately. Cocos2d-html5 allows for the standard update method, where those calculations are scheduled for every animation frame. We want to get smoother results in this app, so we're going to combine two important ideas instead: Box2DWeb and web workers.

Box2DWeb is a physics engine that handles concepts such as gravity, collisions, rotation and so on, so that we don't need to figure them out.

A web worker is a script you can use to run your physics calculations in a an applications thread separate from the main thread, where all of your UI elements are rendered. By separating out this mathematical computation, we can avoid having the physics impact frame rates and the responsiveness of the app. Instead of performing calculations once for every animation frame, the web worker lets your main application thread get the most up-to-date calculations available and use those.

This section is a bit more involved, so let's get started. First, we need to set up a skeleton web worker and populate it with the physics engine. We can then use the physics engine data to update your scene.

Set up Box2DWeb and a web worker

Unzip the Box2DWeb framework you downloaded earlier. Rename the JavaScript file Box2D.js and place it in your js folder.

In the same folder, create a new file and name it Box2dWebWorker.js. Your main implementation will go in this file, but before we can give it any contents, we need to go back and modify SceneStart.js.

Create a global namespace

Open SceneStart.js in your text editor. At the top of the file, create a global namespace. The _g indicates that the namespace is global. Create a variable with the arbitrary name LayerStart. We will use it to reference your root layer later, when we listen for messages from the web worker.

var _g = {
    LayerStart: null
};

Also add the placeholder variable physics for your web worker, above the variables already in place for background, hero, and coins.

    physics:    null,

We want to initialize the global namespace as early in the code as possible. In this case, we place it a few lines down, immediately below where we call the super function.

      _g.LayerStart = this;

Create a web worker

Now that the preliminary initialization is complete, we can create a web worker to send and receive messages.

We assign the placeholder variable we added earlier to be a new web worker. The finished Box2dWebWorker.js file initializes that web worker, so you can send messages from SceneStart.js to Box2dWebWorker.js. Locate the tmx reference to 0-0.xml that we added earlier, and below it, add the following:

      this.physics = new Worker('./js/Box2dWebWorker.js');

Next, we initialize the web worker so that it can receive messages. This initialization allows you to send messages from Box2dWebWorker.js to SceneStart.js, permitting two-way communication between your main application thread and the web worker. Note that we can transmit variables such as strings, numbers, and complex objects, but not functions.

      this.physics.addEventListener('message', function (e) {
      });

Now that the web worker is ready, we send a message to Box2dWebWorker.js to indicate that we want it to initialize itself, and we provide some values. We create four variables with the names msg, walls, coins, and portals. To send and receive messages, we use the msg variable to indicate the action we want to take. The remaining variables are object sets from your tile map that we will use to construct your physics world.

      this.physics.postMessage({
            msg: 'init',
            walls: tmx.getObjectGroup('walls').getObjects(),
            coins: tmx.getObjectGroup('coins').getObjects(),
            portals: tmx.getObjectGroup('portals').getObjects()
      });

Schedule an update function

We now need to set up a scheduled update function to send information about user input to the web worker, so that we can create physics forces on your hero. Locate the code we used to add portals and coins sprites earlier. Below the code that loads the finish portal, but above the return true;, add the following:

      this.schedule(this.update);

Below the return true;, we need to place a comma after the existing } to separate the previous ctor function from our new update function.

    },

    update: function () {
    }

Save the file.

Implement the web worker

For the changes in SceneStart.js to work, we need to implement the web worker in Box2dWebWorker.js. Open the empty file in your text editor.

First, we import an external script. In this case, we use the Box2D.js script, which contains all of the physics functionality we need.

importScripts('./Box2D.js');

Next, we add an event listener, which is a required function for almost all web workers, so that the file can receive incoming messages. As we saw earlier, we rely on a msg variable to indicate our actions. We’re already familiar with the 'init' message from when we set it in SceneStart.js. We will set the 'ApplyImpulse' message in that file later, to provide user input data on impulse from the main application thread. In this file, self essentially means this.

self.addEventListener('message', function (e) {
    if (e.data.msg === 'ApplyImpulse') {
        self.hero.j = e.data.j;
    } else if (e.data.msg === 'init') {
        self.init(e.data);
    }
});

We add the init function, which accepts objects that are passed in from the message. In this case, the objects are the walls, coins, and portals. We also define four placeholder variables for fixtures, bodies, individual objects, and a counter. We can apply the fixtureDef class to an object to define its physics properties, such as density, friction, and restitution. We can apply the bodyDef class to an object to set its position and velocity, and apply impulse.

self.init = function (objects) {
    var fixtureDef, bodyDef, object, n;

Next, we set some global world values. We create the world and provide a 2-D vector that describes the direction of gravity. We also set a global scale value. This value is important because the physics world exists on a smaller scale than our screen dimensions, and in this case it's 32 times smaller. If the physics world was not scaled down, we would have massive objects and the physics might not behave how we want them to. For most apps, a global scale value around 30 will be ideal, but you may need to adjust this.

We also create an empty array, where we can store objects we want the app to remove from the scene. In this case, coins are removed when the hero touches them. We don't want to remove an object in the middle of a physics calculation, and this array keeps a queue of items to remove between calculations.

    self.world = new Box2D.Dynamics.b2World(
        new Box2D.Common.Math.b2Vec2(0.0, 24.0),
        true
    );
    self.world.scale = 32.0;
    self.remove = [];

Now we set some global physics properties for fixtures. Note that setting the friction to 0.0 prevents your hero from sticking to walls. It also causes the hero to slide continually along surfaces, which we'll correct later. The restitution is a bounce value, which we also set to 0.0 because we don’t want the hero to bounce when it hits a wall or surface. Finally, we create a new body variable, which we will reuse to generate physics objects.

    fixtureDef              = new Box2D.Dynamics.b2FixtureDef();
    fixtureDef.density      = 1.0;
    fixtureDef.friction     = 0.0;
    fixtureDef.restitution  = 0.0;
    bodyDef                 = new Box2D.Dynamics.b2BodyDef();

The first physics objects we generate are walls. We cycle through the tile map objects and, based on the dimensions and position of each object, we create a corresponding object in the physics world. By using the b2_staticBody body type, we indicate that these objects are not affected by forces, such as gravity, and remain stationary. We also assign a rectangular shape.

    for (n = 0; n < objects.walls.length; n = n + 1) {
        object                  = objects.walls[n];
        bodyDef.type            = Box2D.Dynamics.b2Body.b2_staticBody;
        bodyDef.position.x      = (object.x + object.width / 2.0) /
                                  self.world.scale;
        bodyDef.position.y      = -(object.y + object.height / 2.0) /
                                  self.world.scale;
        fixtureDef.shape        = new Box2D.Collision.Shapes
                                  .b2PolygonShape();
        fixtureDef.shape.SetAsBox(object.width / 2.0 / self.world.scale,
             object.height / 2.0 / self.world.scale);
        self.world.CreateBody(bodyDef).CreateFixture(fixtureDef)
            .SetUserData({});
    }

Generating coins is very similar, but to create the body here, we call SetUserData and assign a tagName and index, which helps us keep track of coins as the hero collides with them.

    for (n = 0; n < objects.coins.length; n = n + 1) {
        object                  = objects.coins[n];
        bodyDef.type            = Box2D.Dynamics.b2Body.b2_staticBody;
        bodyDef.position.x      = (object.x + object.width / 2.0) /
                                  self.world.scale;
        bodyDef.position.y      = -(object.y + object.height / 2.0) /
                                  self.world.scale;
        fixtureDef.shape        = new Box2D.Collision.Shapes
                                  .b2PolygonShape();
        fixtureDef.shape.SetAsBox(object.width / 2.0 / self.world.scale,
            object.height / 2.0 / self.world.scale);
        object = self.world.CreateBody(bodyDef).CreateFixture(fixtureDef)
                 .SetUserData({
            tagName: 'coin',
            index: n
        });
    }

Speaking of the hero, we need to add a hero to the physics world. Instead of defining a b2_staticBody body type here, we define a b2_dynamicBody one, so that the body we create is affected by the world physics.

Remember that in the previous section you created a sprite only for the finish portal. Here, we use the data of the start portal to determine where your hero is added to the world. We want the main application thread to access the hero from multiple locations in this web worker, so we create a reference to self.hero. We also create a self.hero.j array, which holds the horizontal (x) and vertical (y) impulses that act on the hero.

Because your hero starts in midair, we initialize the number of objects in contact with the hero at 0.

    bodyDef.type            = Box2D.Dynamics.b2Body.b2_dynamicBody;
    bodyDef.position.x      = (objects.portals[0].x + 
                              objects.portals[0].width / 2.0) / 
                              self.world.scale;
    bodyDef.position.y      = -(objects.portals[0].y + 
                              objects.portals[0].height / 2.0) / 
                              self.world.scale;
    fixtureDef.shape        = new Box2D.Collision.Shapes
                              .b2PolygonShape();
    fixtureDef.shape.SetAsBox(28.0 / 2.0 / self.world.scale, 28.0 / 2.0
               / self.world.scale);
    self.hero = self.world.CreateBody(bodyDef);
    self.hero.CreateFixture(fixtureDef).SetUserData({});
    self.hero.j = [];
    self.hero.contacts = 0;

Let’s look at how we can listen for these collisions. First, we create a new Box2D.Dynamics.b2ContactListener and then define the BeginContact function, which is triggered on any new collision. If an object in the collision is a coin, we add that coin to the queue in the removal array.

Note that we call postMessage to notify your main application thread that the coin object is being removed from the physics world, because we also need to remove the coins sprite being rendered in the Cocos2d-html5 framework.

    self.listener = new Box2D.Dynamics.b2ContactListener();
    self.listener.BeginContact = function (contact) {
        self.hero.contacts++;

        if (contact.m_fixtureB.GetUserData().tagName === 'coin') {
            self.remove.push(contact.m_fixtureB.GetBody());
            self.postMessage({
                msg: 'remove',
                index: contact.m_fixtureB.GetUserData().index
            });
        } else if (contact.m_fixtureA.GetUserData().tagName === 
                  'coin') {
            self.remove.push(contact.m_fixtureA.GetBody());
            self.postMessage({
                msg: 'remove',
                index: contact.m_fixtureA.GetUserData().index
            });
        }
    };

We also implement EndContact and decrement the collisions counter any time the hero is no longer touching an object. By incrementing when BeginContact is triggered and decrementing when EndContact is triggered, we know the hero is in midair when it is touching 0 objects. Knowing this number helps us later, when we set limits on the hero's ability to jump at any particular moment. In this case, we want the hero to jump only when it's in contact with a surface or wall.

    self.listener.EndContact = function () {
        self.hero.contacts--;
    };

Now that the contact listeners are defined, we need to set a contact listener for the world, so that our functions are triggered.

    self.world.SetContactListener(self.listener);

Finally, we make two separate calls to setInterval. The first call updates the physics 60 times per second. The second call removes any queued objects from the world and is made even more frequently, to make sure removals are addressed as needed.

    setInterval(self.update, 0.0167);
    setInterval(self.cleanup, 0.0111);
};

We define how cleanup occurs by selecting any queued objects and invoking the DestroyBody function on them.

self.cleanup = function () {
    var n;

    for (n = 0; n < self.remove.length; n = n + 1) {
        self.world.DestroyBody(self.remove[n]);
        self.remove[n] = null;
    }
    self.remove = [];
};

Run the physics calculations

We need to add one more function to Box2dWebWorker.js. The physics calculations run in self.update each cycle, and that data is sent back to the main application thread. Before we begin, let's look at this method in its simplest form, if the forces in the physics world were updated based on an assumed 60 frames per second and then the function was cleaned up:
self.update = function () {
    self.world.Step(
        0.0167,
        20,
        20
    );

    self.world.ClearForces();
    self.hero.j = [0.0, 0.0];
};

For this app, we need to add a little more to the function to make your hero behave the way we want in the physics world. We make four key modifications:

  • Prevent vertical impulse if the hero is not in contact with any objects, to make sure the hero can't jump while it's in midair.
  • Apply the adjusted impulse to the hero to provide its movement.
  • Set the horizontal velocity to 0 when the user is not providing touch input. Because we removed the friction from our static objects earlier, this setting is needed to prevent the hero from sliding on surfaces.
  • Restrict horizontal velocity to between -5.0 and 5.0 units. When the user is providing touch input, the impulse acts continuously on the hero, so this restriction prevents the hero from gaining too much velocity.
We perform these changes before our call to the self.world.Step() function.
    if (self.hero.contacts === 0) {
        self.hero.j[1] = 0.0;
    }

    self.hero.ApplyImpulse(
        new Box2D.Common.Math.b2Vec2(self.hero.j[0], self.hero.j[1]),
        self.hero.GetWorldCenter()
    );

    if (self.hero.j[0] === 0) {
        self.hero.SetLinearVelocity(
            new Box2D.Common.Math.b2Vec2(
                0.0,
                self.hero.GetLinearVelocity().y
            )
        );
    }

    self.hero.SetLinearVelocity(
        new Box2D.Common.Math.b2Vec2(
            Math.max(-5.0, Math.min(self.hero.GetLinearVelocity()
                .x, 5.0)),
            self.hero.GetLinearVelocity().y
        )
    );
The last thing we do is take the data calculated by the function and send it back to your main application thread, so we can use it to update the position and rotation of the sprites. In this case, we don't need to communicate a msg, because the presence of a hero object indicates our action instead. In this message, we supply the new x and y coordinates and rotation of the hero.
    
    self.postMessage({
        hero: {
            x: self.hero.GetPosition().x * self.world.scale,
            y: -self.hero.GetPosition().y * self.world.scale,
            r: self.hero.GetAngle()
        }
    });
};

Save the file.

Send impulse information

We can now go back into SceneStart.js to complete the code. Locate the update function we set up earlier. Between the {}, we now fully implement the scheduled update function to send impulse information to your web worker. Until we implement the touch controls, the impulse is always zero.

In Box2dWebWorker.js, you implemented an 'ApplyImpulse' message listener. We send messages to that listener from here. For every animation frame, we send the current impulse of the hero to the web worker, so that it can calculate the physics. User input affects the impulse, and we haven't implemented any user input yet, so currently gravity is the only external force acting on the hero.
        this.physics.postMessage({
            msg: 'ApplyImpulse',
            j: this.hero.j
        });
        this.hero.j[1] = 0.0;
Locate the code below, which we used to initialize the web worker earlier.
        this.physics.addEventListener('message', function (e) {
        });

Between the {}, we listen for any messages the web worker sends to the main application thread, and then apply the attached data to the hero.

The first message we need to check for is the presence of a hero object. If that object exists, we know that we have new position or rotation data and we call the setPosition and setRotation functions on our hero sprite.

The only other message we could potentially get is a 'remove' message. If we get that message, we remove the coins sprite from the scene and clean up any references to that object. We also decrement the coin sprite counter. If the counter reaches 0, we know that the hero collided with and removed all of the coins in your scene. If this is the case, we run a Cocos2d-html5 action to fade your finish portal into view.
            if (e.data.hero) {
                _g.LayerStart.hero.setPosition(new cc.Point(
                    e.data.hero.x,
                    e.data.hero.y
                ));
                _g.LayerStart.hero.setRotation(e.data.hero.r / 
                   (Math.PI * 2.0) * 360.0);
            } else if (e.data.msg === 'remove') {
                _g.LayerStart.removeChild(_g.LayerStart.coins
                  .sprites[e.data.index]);
                _g.LayerStart.coins.sprites[e.data.index] = null;
                _g.LayerStart.coins.sprites.count = _g.LayerStart
                  .coins.sprites.count - 1;

              if (_g.LayerStart.coins.sprites.count === 0) {
                  _g.LayerStart.finish.runAction(cc.FadeTo
                    .create(2.0, 255.0));
              }
            }

Save the file.

Test your app in the Ripple emulator

Good work! Your Cocos2d-html5 app should now run on a working physics engine. Return to your start page in the Ripple emulator.


The app running in the Ripple emulator.

The only visible difference from the last time we checked your progress is that your hero is no longer partially concealed in the corner of the screen, and instead appears a few blocks above the lower-left surface. We still can't interact with the hero, but in the final section of this tutorial, you will set up touch controls to make the game playable.

Last modified: 2014-03-10



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus