Talking about Box2D, HTML5, Javascript and Phaser.
Back in 2011 I tried to build a script to slice, split and cut Box2D bodies using AS3, then two years ago I updated the script to work with Phaser and Matter.js physics.
Now that Box2D is living again thanks to Planck.js, it’s time to update the script to make it work with the latest libraries.
Like in my previous example, I am using the awesome PolyK library which allows us to work with polygons quite easily.
Let’s see the script in action:
Drag the mouse to draw the line and cut that box.
How does it work? You should know how to draw a line with the mouse, and it’s quite boring to explain so let’s jump straight to the point:
* Each time you draw a line, we know its start and end points
* We loop through all Box2D bodies and get their shape vertices. At this time, bodies are just polygons
* We check if the line splits in two the polygon thanks to PolyK’s Slice
method.
* If the result of Slice
method consists in two – or more – polygons, then the original polygon has been sliced!
* We remove the sliced Box2D body.
* We calculate the centroid of each polygon born from the sliced body
* We finally create the new Box2D bodies from the vertices of the resulting poligons.
Look at the source code, uncommented but quite easy to understand:
let game;
let gameOptions = {
worldScale: 30
}
window.onload = function() {
let gameConfig = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "thegame",
width: 600,
height: 600
},
scene: playGame
}
game = new Phaser.Game(gameConfig);
window.focus();
}
class playGame extends Phaser.Scene {
constructor() {
super("PlayGame");
}
create() {
let gravity = planck.Vec2(0, 3);
this.world = planck.World(gravity);
this.debugDraw = this.add.graphics();
this.createBox(game.config.width / 2, game.config.height - 20, game.config.width, 40, false);
this.createBox(game.config.width / 2, game.config.height / 2, game.config.width / 2, game.config.height / 2, true);
this.lineGraphics = this.add.graphics();
this.input.on("pointerdown", this.startDrawing, this);
this.input.on("pointerup", this.stopDrawing, this);
this.input.on("pointermove", this.keepDrawing, this);
this.isDrawing = false;
}
startDrawing() {
this.isDrawing = true;
}
keepDrawing(pointer) {
if (this.isDrawing) {
this.lineGraphics.clear();
this.lineGraphics.lineStyle(1, 0x00ff00);
this.lineGraphics.moveTo(pointer.downX, pointer.downY);
this.lineGraphics.lineTo(pointer.x, pointer.y);
this.lineGraphics.strokePath();
}
}
stopDrawing(pointer) {
this.lineGraphics.clear();
this.isDrawing = false;
let toBeSliced = [];
let toBeCreated = [];
for (let body = this.world.getBodyList(); body; body = body.getNext()) {
let pointsArray = [];
for (let fixture = body.getFixtureList(); fixture; fixture = fixture.getNext()) {
let shape = fixture.getShape();
let vertices = shape.m_vertices.length;
for (let i = 0; i < vertices; i ++) {
let vertex = shape.getVertex(i);
let worldPosition = body.getWorldPoint(vertex);
pointsArray.push(worldPosition.x * gameOptions.worldScale, worldPosition.y * gameOptions.worldScale);
}
}
let slicedPolygons = PolyK.Slice(pointsArray, pointer.downX, pointer.downY, pointer.upX, pointer.upY);
if (slicedPolygons.length > 1) {
toBeSliced.push(body);
slicedPolygons.forEach (function(points) {
toBeCreated.push(points)
})
}
}
toBeSliced.forEach (function(body) {
this.world.destroyBody(body)
}.bind(this));
toBeCreated.forEach(function(points) {
let vertices = [];
for (let i = 0; i < points.length / 2; i ++) {
vertices.push(planck.Vec2(points[i * 2] / gameOptions.worldScale, points[i * 2 + 1] / gameOptions.worldScale))
}
let polygon = planck.Polygon(vertices);
let centroid = polygon.m_centroid;
let body = this.world.createBody();
body.setDynamic();
body.setPosition(planck.Vec2(centroid.x, centroid.y));
for (let i = 0; i < vertices.length; i ++) {
vertices[i].x -= centroid.x;
vertices[i].y -= centroid.y;
}
body.createFixture(planck.Polygon(vertices));
body.setMassData({
mass: 1,
center: planck.Vec2(),
I: 1
});
}.bind(this))
}
createBox(posX, posY, width, height, isDynamic) {
let box = this.world.createBody();
if (isDynamic) {
box.setDynamic();
}
box.createFixture(planck.Box(width / 2 / gameOptions.worldScale, height / 2 / gameOptions.worldScale));
box.setPosition(planck.Vec2(posX / gameOptions.worldScale, posY / gameOptions.worldScale));
box.setMassData({
mass: 1,
center: planck.Vec2(),
I: 1
});
return box;
}
update(t, dt) {
this.world.step(dt / 1000 * 2);
this.world.clearForces();
this.debugDraw.clear();
this.debugDraw.lineStyle(2, 0xff00ff);
this.debugDraw.fillStyle(0xff00ff, 0.1);
for (let body = this.world.getBodyList(); body; body = body.getNext()) {
this.debugDraw.beginPath();
for (let fixture = body.getFixtureList(); fixture; fixture = fixture.getNext()) {
let shape = fixture.getShape();
let vertices = shape.m_vertices.length;
for (let i = 0; i < vertices; i ++) {
let vertex = shape.getVertex(i);
let worldPosition = body.getWorldPoint(vertex);
if (i == 0) {
this.debugDraw.moveTo(worldPosition.x * gameOptions.worldScale, worldPosition.y * gameOptions.worldScale);
}
else {
this.debugDraw.lineTo(worldPosition.x * gameOptions.worldScale, worldPosition.y * gameOptions.worldScale);
}
}
}
this.debugDraw.closePath();
this.debugDraw.strokePath();
this.debugDraw.fillPath();
}
}
}
A quite complex task has been accomplished in a few lines, let’s see if you manage to sort something interesting out of it. Download the source code.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.