2dx_icon_512_rounded

Cocos2dx-js + Chipmunk – Part 5: Creating polygon physics object with PhysicsEditor

Alright, you know already how to create circle, rectangle and square physics object. But what if our physics sprite is created from complex sprite? In this tutorial I will show you how to use PhysicsEditor to do that.

As always, I assume that you have project created with previous parts of this tutorial.

1. Lion.png
Here you have sprite representing complex shape – lion.png. We will use it to create our physics object. Copy it and paste to your $PROJECT-DIR/Resources/images/resources-auto

 lion

2. How to use PhysicsEditor

  • PhysicsEditor is a great tool to trim sprites to their real shapes, without transparency. Install it from this page. It is paid tool but it is worth to spend some money. Moreover it has 14-days trial so you can learn with no cash.
  • Install PhysicsEditor
  • Open it
  • #1 Add lion.png to the editor by clicking ‘add image’ icon on the right-bottom of the left column

pe1

 

  • #2 Select exporter as Chipmunk generic (PLIST) – BETA
  • #3 Click ‘Shape tracer’ button to open editor

pe2

  • #4 You can see quite smooth contour over lion’s shape. We don’t need that quality – less polygons, less power needed to physics simulations. Set tolerance to 10.
  • #5 Our image has a little blur effect. To ignore that we need to set Alpha treshold to 6.
  • #6 You should see that. Click Ok.

pe3

 

  • Now we need to edit some edges on the preview. Lion has to be able stay on his paws.

pe4

 

  • Click ‘Publish’ button on the left top and save file as lion.plist in the images folder

3. Function to read lion.plist file – reading polygons
To our project to js folder let’s add new .js file – GCpShapeCache.js. It is set of functions written by Andreas Loew. These functions will allow us to read lion.plist file, create dictionary with polygons and finally create our object. Here is the code:

//
//  GCpShapeCache.js
//
//  All rights reserved.
//
//  Loads physics sprites created with http://www.PhysicsEditor.de
//
//  Generic Shape Cache for Chipmunk
//
//  Copyright by Andreas Loew
//      http://www.PhysicsEditor.de
//      http://texturepacker.com
//      http://www.code-and-web.de
//
//  All rights reserved.
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

var gcp = gcp || {};
var bodyDefs;

gcp._pointFromString = function(str)
{
    var coords = str.replace(/[{}]/g, "").trim().split(",");
    return cc.p(parseFloat(coords[0]),parseFloat(coords[1]));
};

/**
 * Shape cache
 * This class holds the shapes and makes them accessible
 */
gcp.ShapeCache = cc.Class.extend({

    bodyDefs: null,

    /**
     * Constructor
     */
    ctor:function () {
        bodyDefs = [];
    },

    /**
     * Adds shapes to the shape cache
     * @param plist name of the plist file to load
     * @result false in case of error
     */
    addShapesWithFile: function (plist) {
    	cc.log("WCZYTUJE: "+plist);
        var dictionary = cc.FileUtils.getInstance().dictionaryWithContentsOfFile(plist);
        if(!dictionary)
        	return false;

        var metadataDict = dictionary["metadata"];
        var format = parseInt(metadataDict["format"]);

        if(format != 1)
        	return false;

        var bodyDict = dictionary["bodies"];

        for(var bodyName in bodyDict)
        {
        	//cc.log("BODY: "+bodyName);
            var bodyData = bodyDict[bodyName];
            var bodyDef = new g.BodyDef();
			bodyDef.anchorPoint = null;
			bodyDef.momentum = 0.0;
			bodyDef.fixtures = [];

            bodyDefs[bodyName] = bodyDef;
            bodyDef.anchorPoint = gcp._pointFromString(bodyData["anchorpoint"]);

            var fixtureList = bodyData["fixtures"];
            var totalMass = 0.0;
            var totalBodyMomentum = 0.0;

            for(var fixtureIndex in fixtureList)
            {
            	var fixtureData = fixtureList[fixtureIndex];
				//cc.log("FIXTURE INDEX: "+fixtureIndex+"FIXTURE DATA: "+fixtureData);

                var fd = new g.FixtureData();
                fd.polygons = [];
                fd.momentum = 1.0;
                fd.area = 0.0;

                if(!fd)
                	return false;

                bodyDef.fixtures.push(fd);

                fd.friction = parseFloat(fixtureData["friction"]);
                fd.elasticity = parseFloat(fixtureData["elasticity"]);
                fd.mass = parseFloat(fixtureData["mass"]);
                fd.surfaceVelocity = gcp._pointFromString(fixtureData["surface_velocity"]);
                fd.layers = parseInt(fixtureData["layers"]);
                fd.group = parseInt(fixtureData["group"]);
                fd.collisionType = parseInt(fixtureData["collision_type"]);
                fd.isSensor = fixtureData["fixtureData"] === "true";

                var fixtureType = fixtureData["fixture_type"];

                var totalArea = 0.0;

               totalMass += fd.mass;

               if(fixtureType === "POLYGON")
                {
                    var polygonsArray = fixtureData["polygons"];

					//cc.log("POLYGONS ARRAY: "+polygonsArray+" LENGTH: "+polygonsArray.length);

					for(var polygonIndex in polygonsArray)
                    {
                        var polygonArray = polygonsArray[polygonIndex];

						//cc.log("POLYGON ARRAY: "+polygonArray+" OF INDEX: "+polygonIndex);

						var poly = new g.Polygon();
                        poly.vertices = null;
                        poly.numVertices = 0;
                        poly.area = 0.0;
                        poly.momentum = 0.0;

                        if(!poly)
                        	return false;

                        fd.polygons.push(poly);

                        poly.numVertices = polygonArray.length;
                        //cc.log("poly num vertices: "+poly.numVertices);
                        var vertices = poly.vertices = new Array(poly.numVertices * 2);
                        //cc.log("V: "+vertices);
                        //cc.log(poly.vertices);
                        //cc.log(new Array(poly.numVertices * 2));

                        if(!vertices)
                        	return false;

                        var tempVerts = [];
                        var vindex = 0;
                        for(var pointStringIndex in polygonArray)
                        {
                            var pointString = polygonArray[pointStringIndex];
                            //cc.log("PS: "+pointString);
                            var offset = gcp._pointFromString(pointString);
                           // cc.log("RESULT: "+gcp._pointFromString(pointString));
                            //cc.log(offset.x);
                            vertices[vindex] = offset.x / TEXTURES_SCALE_FACTOR;
                            //cc.log(offset.x / TEXTURES_SCALE_FACTOR);
                           // cc.log("AAAAAA: "+vertices[vindex]);
                            vertices[vindex+1] = offset.y / TEXTURES_SCALE_FACTOR;
                            tempVerts.push(cp.v(offset.x / TEXTURES_SCALE_FACTOR,offset.y / TEXTURES_SCALE_FACTOR));
                            vindex+= 2;
                        }

						//cc.log("===== verts: "+vertices);
                        poly.area = cp.areaForPoly(vertices);
                        //cc.log("POLY AREA: "+poly.area);
						totalArea += poly.area;
                    }
                }
                else
                {
                    cc.Assert(0)
                }
				fd.area = totalArea;

                var totalFixtureMomentum = 0.0;

				//cc.log("TOTAL AREA: " +totalArea);
				//cc.log("BODY POLYGONS LEN: "+fd.polygons.length);
                if(totalArea)
                {
                    for(var pIndex in fd.polygons)
                    {
                        var p = fd.polygons[pIndex];
						//cc.log("POL: "+p);
                        p.mass = (p.area * fd.mass) / fd.area;

                        p.momentum = cp.momentForPoly(p.mass, p.vertices, cc.p(0,0));
                        //cc.log("OBLICZONY MOMEMTUM: "+p.momentum);

                        totalFixtureMomentum += p.momentum;
                    }
                }
                fd.momentum = totalFixtureMomentum;
                totalBodyMomentum = totalFixtureMomentum;
            }

            bodyDef.mass = totalMass;
            bodyDef.momentum = totalBodyMomentum;
        }

        return true;
    },

    /**
     * Creates a body with the given name in the given space.
     * @param name name of the body
     * @param space pointer to the space
     * @param data data to set in the body
     * @result new created body
     */
    createBodyWithName: function (name, space, data) {
    	var bd = bodyDefs[name];

        if(!bd)
        	return 0;

        var body = new cp.Body(bd.mass, bd.momentum);

        body.p = bd.anchorPoint;
        body.data = data;

        space.addBody(body);

        for(var fdIndex in bd.fixtures)
        {
        	var fd = bd.fixtures[fdIndex];

			for(var pIndex in fd.polygons)
            {
                var p = fd.polygons[pIndex];

                var shape = new cp.PolyShape(body, p.vertices, cc.p(0,0));

                shape.e = fd.elasticity;
                shape.u = fd.friction;
                shape.surface_v = fd.surfaceVelocity;
                shape.collision_type = fd.collisionType;
                shape.group = fd.group;
                shape.layers = fd.layers;
                shape.sensor = fd.isSensor;

                space.addShape(shape);
            }
        }
        return body;
    },

    /**
     * Returns the anchor point of the given sprite
     * @param shape name of the shape to get the anchorpoint for
     * @return anchorpoint
     */
    anchorPointForShape: function (shape) {
        var bd = bodyDefs[shape];
        if (bd != null)
        return bd.anchorPoint;
        else return null;
    }

});

gcp.s_sharedShapeCache = null;

/**
 * Returns the shared instance of the Shape cache
 * @return {gcp.ShapeCache}
 */
gcp.ShapeCache.getInstance = function () {
    if (!gcp.s_sharedShapeCache) {
        gcp.s_sharedShapeCache = new gcp.ShapeCache();
    }
    return gcp.s_sharedShapeCache;
};

/**
 * Purges the cache. It releases all the Sprite Frames and the retained instance.
 */
gcp.ShapeCache.purgeSharedShapeCache = function () {
    gcp.s_sharedShapeCache = null;
};

var g = g || {};

g.Polygon = cc.Class.extend({
    vertices:null,
    numVertices:0,
    area:0.0,
    mass:0.0,
    momentum:0.0
});

/**
 * Fixture definition
 * Holds fixture data
 */
g.FixtureData = cc.Class.extend({
    mass:0.0,
    elasticity:0.0,
    friction:0.0,
    surfaceVelocity:null,
    collisionType:null,
    group:null,
    layers:null,
    area:0.0,
    momentum:0.0,
    isSensor:false,
    polygons:[]
});

/**
 * Body definition
 * Holds the body and the anchor point
 */
g.BodyDef = cc.Class.extend({
    anchorPoint:null,
    fixtures:[],
    mass:0.0,
    momentum:0.0
});

4. Next functions
Add these lines:

	bodyDefs = [];
	shapeCache = gcp.ShapeCache.getInstance();
	shapeCache.addShapesWithFile("lion.plist");
	addPhysicsPoly("lion.png");

to your init method and over the init function, add

	var bodyDefs;

variable.
For me it looks like this:

var bodyDefs;

PhysicsScene.prototype.init = function() {
	cc.Director.getInstance().setDisplayStats(false);
	initPhysics();
	initDebugMode(controller);

	addWallsAndGround();
	addPhysicsCircle();
	addPhysicsBox("square.png");
	addPhysicsBox("rectangle.png");

	bodyDefs = [];
	shapeCache = gcp.ShapeCache.getInstance();
	shapeCache.addShapesWithFile("lion.plist");
	addPhysicsPoly("lion.png");
}

Add last 2 new functions below your code:

function addPhysicsPoly(filename) {
	cc.log("Physics poly for: "+filename);
	sprite = cc.Sprite.create(filename);
	controller.addChild(sprite);
	sprite.setPosition(cc.p(winSize.width * Math.random(), winSize.height * Math.random()));
	phObj = addPolyObject(controller, sprite, filename, bodyDefs[filename]);

}

function addPolyObject(controller, obj, spriteFileName, bd) {
	var nodeSize = obj.getContentSize(),
		phNode = cc.PhysicsSprite.create(spriteFileName),
		phBody = null,
		phShape = null,
		scale = 1;
	nodeSize.width *= scaleX;
	nodeSize.height *= scaleY;

	phBody = space.addBody(new cp.Body(mass, bd.momentum));
	phBody.p = bd.anchorPoint;
	phNode.setAnchorPoint(bd.anchorPoint);

	vzero = cp.vzero;
	bdPolygons = bd.fixtures[0].polygons;

	shapes = [];
	for (j = 0; j < bdPolygons.length; j++) {
		polyShape = new cp.PolyShape(phBody, bdPolygons[j].vertices, vzero);

		shape = space.addShape(polyShape);

		shape.setFriction(0.5);
		shape.setElasticity(0.5);
		shapes.push(shape);
	}

	phNode.setBody(phBody);
	phNode.setPosition(obj.getPosition());

	controller.addChild(phNode);
	//obj.setVisible(false);
}

5. Compile and run
Let’s see what happen:
physicsPart2

 

Sourcehttps://bitbucket.org/galante/chipmunktutorialresources/commits/branch/master