How to create physics shapes for Panda2 and P2 physics

Andreas Löw
Last updated:
GitHub
How to create physics shapes for Panda2 and P2 physics

In this tutorial you are going to learn

  • How to set up Panda 2 with Physics (P2 engine)
  • How to create basic physical shapes (circles, rectangles)
  • How to create complex physical shapes (polygon based)
  • How to use PhsicsSprite for a simple integration of physics
  • How to create your own classes which give you even more control over the physics simulation

Getting started with Physics

Creating the basic scene

Please download the tutorial repo from GitHub. It contains a sprite sheet with some object we are going to play with. If you want to replace the sprites with your own you might want to follow our Panda 2 sprite sheet tutorial

Let's start with a simple scene in main.js:

game.module(
    'game.main'
)
.require(
    'plugin.p2'
)
.body(function() {
    
game.addAsset('objects.atlas');

game.createScene('Main', {
    init: function() {
        this.backgroundColor = '#cceeff';
        this.makeFloor();
    },
    
    makeFloor: function() {
        for(var i=0; i<8; i++) {
            var floor = new game.Sprite('floor.png');
            floor.position.set(i*floor.width, game.height-floor.height);
            floor.addTo(this.stage);
        }
    }
});

This scene has nothing to do with physics yet — it's just some floor and background.

Let's now add a simple object that will be controlled by the physics engine.

First you have to initialize the physics engine. Add this line to the top of the init() method:

    init: function() {
        this.world = new game.Physics();
        this.backgroundColor = '#cceeff';
        this.makeFloor();
    },

The first physics object: A crate

Now add a PhysicsSprite object. It takes the crate sprite from the sprite sheet, places it in the center of the screen. The shape used for this object is a rectangle in the size of the sprite.

    init: function() {
        this.world = new game.Physics();
        this.backgroundColor = '#cceeff';
        this.makeFloor();

        var crate = new game.PhysicsSprite("crate.png", game.width/2, 100);
        crate.addTo(this.stage);
    },

Nothing is happening. The crate is hovering in the air. Why? This is because the default for an object is a mass of 0 which means: Static object.

Now set the mass property:

        var crate = new game.PhysicsSprite("crate.png", game.width/2, 100, {mass:1});
        crate.addTo(this.stage);

Nice. Crate is dropping. But not stopped by the floor... now change the Sprite class in makeFloor() into PhysicsSprite.

    makeFloor: function() {
        for(var i=0; i<8; i++) {
            var floor = new game.PhysicsSprite('floor.png');
            floor.position.set(i*floor.width, game.height-floor.height);
            floor.addTo(this.stage);
        }
    }

Hm. That's not working (currently). The floor shapes are now all top left :-(

This is because there are now 2 coordinate systems in play: The visual for the sprites and the physics coordinate system. PhysicsSprite updates the visual coordinates from the simulation and overwrites the position settings. I'll explain that in detail later.

For now use the constructor syntax. There's unfortunately no way to access the sprite's size since it was not yet constructed. But we know that it is 128x128 pixels.

Another difference between Sprite and PhysicsSprite is the anchor point: The default anchor point for the physics shape is the center, this is why we have to add size/2 for the correct position:

    makeFloor: function() {
        for(var i=0; i<8; i++) {
            var size = 128;
            var floor = new game.PhysicsSprite('floor.png', i*size+size/2, game.height-floor.height+size/2 );
            floor.addTo(this.stage);
        }
    }

Banana in a box: Physics debugging

Nice! One crate is not enough? Let's add more stuff — we've got banana sprites. Add the following method. The function mousedown() called for each mouse click (or tap) and adds a new banana to the scene:

    mousedown: function(x, y) {
        var banana = new game.PhysicsSprite('banana.png', x, y, { mass: 1});
        banana.addTo(this.stage);
    },

I agree: The result is unexpected:

2d physics scene with objects

Why don't these bananas work as expected? Let's enable physics debugging in Panda2 to see what's going wrong. Click on the chain icon at the bottom of the preview.

Debugging 2d physic

Ok — Do you remember what I've said before: PhysicsSprite uses the size of the sprite to create a box shape. That's what we got. A banana in a box!

You now have to create a better shape for the banana. That's a bit tricky... how to do that? Using PhotoShop to pick vertex coordinates? No way!

PhysicsEditor

I've got a nice tool for you: PhysicsEditor. Please download and install it from here:

You can use the trial version for 7 days. This should be long enough for you to complete the tutorial and play with it ;-)

Editing 2d physics objects with PhysicsEditor

You have to perform the following steps in order:

  1. Select the Exporter Panda 2
  2. Drop all sprites from assets/objects onto the left panel. Important for this is that you add the single sprites from the asset folder. Do not add the sprite sheet from media. . Select the banana shape — it'll appear in the center screen.
  3. Click the magic wand icon in the tool bar

A new window opens with the shape tracer.

Tracing physics shapes

The main parameter to play with in this screen is Tolerance: It influences how the shape is created. A value too high gives you a shape that does not match your sprite too well. A value too low gives you too many vertices. This might result in very high CPU usage and bad performance of you game.

A tolerance of 4 or 5 should give you a good result:

bad trace not enough

Bad shape: The polygon has not enough vertices to match the sprite.

bad trace too many

Bad shape: This polygon has too many vertices. This wastes CPU power.

good trace

Good shape: A decent fit for the sprite.

Close the tracer. You should now see the polygon in the center screen.

To manually edit the polygon:

  • Double-click near a line to create a new vertex
  • Double-click a vertex to delete it
  • Drag vertices to move them

If you are satisfied: Press publish. PhysicsEditor asks you where to put the .json file. Save objects.json in the project's media folder.

Use Save project to store the PhysicsEditor project in the assets folder.

Now go back to Panda. We'll do more work in PhysicsEditor later.

Loading and using shapes in Panda

First of all you have to load the created objects.json file. Add the following line before game.createScene:

game.addAsset('objects.atlas');

game.addAsset('objects.json');

game.createScene('Main', {
...

Now change the mousedown method to the following:

    mousedown: function(x, y) {
        var banana = new game.PhysicsSprite('banana.png', x, y, {
            data: 'objects.json',
            name: 'banana'
        });
        banana.addTo(this.stage);
    },

Ah — much better now. Except that there's still some space above the crate. This is because the crate is still the old box. Another thing to notice is that the crate is easily pushed away by the bananas. Let's change that and make it heavier.

Back to PhysicsEditor and trace the crate. In PhysicsEditor's right sidebar you see shape settings. Change Mass to 100 and click Publish.

Editing 2d collision shapes with PhysicsEditor

While we are here let's also add the other shapes.

For the Orange use the circle shape instead of tracing the sprite:

Creating a circular collision shape with PhysicsEditor

The cherries are best represented by a compound shape:

  • start with tracing the cherries
  • delete all vertices in the lower area by double clicking them
  • add 2 circle shapes
Creating a compound collision shape with PhysicsEditor

Back in Panda hit CTRL-R / CMD-R to reload the scene.

Let's extend the mousedown method to drop random items:

    mousedown: function(x, y) {
        var shapes = ['banana', 'crate', 'cherries', 'orange'];
        var shape = shapes[Math.floor(Math.random() * Math.floor(shapes.length))];
        
        var banana = new game.PhysicsSprite(shape+'.png', x, y, {
            data: 'objects.json',
            name: shape
        });
        banana.addTo(this.stage);
    },

Here's the result for you to play with:

Getting more control with your own GameObject class

PhysicsSprite is a nice convenience class to speed up the development — but it also gives you less control over the object itself. Let's create a GameObject class:

game.createClass('GameObject', {
    init: function(x, y, objectType) {
        this.sprite = new game.Sprite(objectType+'.png');
        this.sprite.anchorCenter();
        this.sprite.addTo(game.scene.stage);
        
        this.body = game.Body.fromData('objects.json', objectType);
        this.body.position[0] = x / game.scene.world.ratio;
        this.body.position[1] = y / game.scene.world.ratio;
        this.body.addTo(game.scene.world);
    },
    
    update: function() {
        this.sprite.position.x = this.body.position[0] * this.body.world.ratio;
        this.sprite.position.y = this.body.position[1] * this.body.world.ratio;
        this.sprite.rotation = this.body.angle;
    }
});

This class combines the sprite, and the physics body in one object.

The init() method creates a sprite and adds it to the game scene, it also created the physics body and adds it to the physics world.

There are 2 important things to note: game.scene.world.ratio and the update() method:

game.scene.world.ratio

The physics simulation and the graphics representation of it both have their own coordinate system. This is important because it makes the simulation independent of the graphics resolution. E.g. you can use big sprites for high resolutions and small sprites for low resolution devices without changing the physics behaviour.

The game.scene.world.ratio is the factor between both coordinate systems.

You have to divide all pixel coordinates by the ratio to get the physics coordinates. On the other hand multiply all physics coordinates with the ratio to get the pixel coordinates.

update()

The update method is called for each frame. It's responsible for updating the position and rotation of the sprite.

creating the new objects

Exchange the previous version of the mousedown() method to use the new game object class:

    mousedown: function(x, y) {
        var shapes = ['banana', 'crate', 'cherries', 'orange'];
        var shape = shapes[Math.floor(Math.random() * Math.floor(shapes.length))];

        new game.GameObject(x, y, shape);
    },