Sempre me animei a mexer com a Box2D e suas variações.
Bom, agora que arrumei um pouco de tempo, vamos pegar o live demo da versão javascript e incrementá-lo para que ele fique de um jeito que eu entenda, hehe.
Mas antes de começarmos, O que é Box2D?
A definição completa você encontra no manual da mesma, mas deixe-me resumir pra você: física razoavelmente convincente para seus jogos 2D! corpos rígidos (i.e. indestrutíveis), gravidade, forças, diversão garantida para toda a família, dos 8 aos 80, ;-) Lembre-se de agradecer ao Erin Catto por fazer isso no tempo livre dele.
A parte importante: Box2D não é uma API de desenho, mas sim de cálculos. Pra desenhar recorra a um SDL, OpenGL, Java2D ou html canvas ou ainda SVG.
Sem mais delongas, vamos começar. Para rodar com sucesso estes exemplos, não precisaremos de muito, apenas de um editor qualquer de código e de um firefox com firebug. ou um chrome.
faça o download do zip contendo o box2dweb:
Descompacte e teremos algo assim:
Você pode ver o funcionamento da box2d abrindo o demo.html
O código original segue com uma ligeira modificação: o bloco de script que ficava dentro do demo.html eu movi para um js separado, mais a frente explico o motivo.
<html>
<head>
Box2dWeb Demo
<script type="text/javascript" src="Box2dWeb-2.1.a.3.min.js"></script>
</head>
<body>
<script type="text/javascript" src="demo.js"></script>
</body>
</html>
O demo.js:
function init() {
var b2Vec2 = Box2D.Common.Math.b2Vec2, b2AABB = Box2D.Collision.b2AABB, b2BodyDef = Box2D.Dynamics.b2BodyDef, b2Body = Box2D.Dynamics.b2Body, b2FixtureDef = Box2D.Dynamics.b2FixtureDef, b2Fixture = Box2D.Dynamics.b2Fixture, b2World = Box2D.Dynamics.b2World, b2MassData = Box2D.Collision.Shapes.b2MassData, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape, b2DebugDraw = Box2D.Dynamics.b2DebugDraw, b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef;
var world = new b2World(new b2Vec2(0, 10) // gravity
, true // allow sleep
);
var fixDef = new b2FixtureDef;
fixDef.density = 1.0;
fixDef.friction = 0.5;
fixDef.restitution = 0.2;
var bodyDef = new b2BodyDef;
// create ground
bodyDef.type = b2Body.b2_staticBody;
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox(20, 2);
bodyDef.position.Set(10, 400 / 30 + 1.8);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.position.Set(10, -1.8);
world.CreateBody(bodyDef).CreateFixture(fixDef);
fixDef.shape.SetAsBox(2, 14);
bodyDef.position.Set(-1.8, 13);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.position.Set(21.8, 13);
world.CreateBody(bodyDef).CreateFixture(fixDef);
// create some objects
bodyDef.type = b2Body.b2_dynamicBody;
for ( var i = 0; i < 10; ++i) {
if (Math.random() > 0.5) {
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox(Math.random() + 0.1 // half width
, Math.random() + 0.1 // half height
);
} else {
fixDef.shape = new b2CircleShape(Math.random() + 0.1 // radius
);
}
bodyDef.position.x = Math.random() * 10;
bodyDef.position.y = Math.random() * 10;
world.CreateBody(bodyDef).CreateFixture(fixDef);
}
// setup debug draw
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite(document.getElementById("canvas").getContext("2d"));
debugDraw.SetDrawScale(30.0);
debugDraw.SetFillAlpha(0.5);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debugDraw);
window.setInterval(update, 1000 / 60);
// mouse
var mouseX, mouseY, mousePVec, isMouseDown, selectedBody, mouseJoint;
var canvasPosition = getElementPosition(document.getElementById("canvas"));
document.addEventListener("mousedown", function(e) {
isMouseDown = true;
handleMouseMove(e);
document.addEventListener("mousemove", handleMouseMove, true);
}, true);
document.addEventListener("mouseup", function() {
document.removeEventListener("mousemove", handleMouseMove, true);
isMouseDown = false;
mouseX = undefined;
mouseY = undefined;
}, true);
function handleMouseMove(e) {
mouseX = (e.clientX - canvasPosition.x) / 30;
mouseY = (e.clientY - canvasPosition.y) / 30;
}
;
function getBodyAtMouse() {
mousePVec = new b2Vec2(mouseX, mouseY);
var aabb = new b2AABB();
aabb.lowerBound.Set(mouseX - 0.001, mouseY - 0.001);
aabb.upperBound.Set(mouseX + 0.001, mouseY + 0.001);
// Query the world for overlapping shapes.
selectedBody = null;
world.QueryAABB(getBodyCB, aabb);
return selectedBody;
}
function getBodyCB(fixture) {
if (fixture.GetBody().GetType() != b2Body.b2_staticBody) {
if (fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(),
mousePVec)) {
selectedBody = fixture.GetBody();
return false;
}
}
return true;
}
// update
function update() {
if (isMouseDown && (!mouseJoint)) {
var body = getBodyAtMouse();
if (body) {
var md = new b2MouseJointDef();
md.bodyA = world.GetGroundBody();
md.bodyB = body;
md.target.Set(mouseX, mouseY);
md.collideConnected = true;
md.maxForce = 300.0 * body.GetMass();
mouseJoint = world.CreateJoint(md);
body.SetAwake(true);
}
}
if (mouseJoint) {
if (isMouseDown) {
mouseJoint.SetTarget(new b2Vec2(mouseX, mouseY));
} else {
world.DestroyJoint(mouseJoint);
mouseJoint = null;
}
}
world.Step(1 / 60, 10, 10);
world.DrawDebugData();
world.ClearForces();
}
;
// helpers
// http://js-tut.aardon.de/js-tut/tutorial/position.html
function getElementPosition(element) {
var elem = element, tagname = "", x = 0, y = 0;
while ((typeof (elem) == "object")
&& (typeof (elem.tagName) != "undefined")) {
y += elem.offsetTop;
x += elem.offsetLeft;
tagname = elem.tagName.toUpperCase();
if (tagname == "BODY")
elem = 0;
if (typeof (elem) == "object") {
if (typeof (elem.offsetParent) == "object")
elem = elem.offsetParent;
}
}
return {
x : x,
y : y
};
}
};
init();
Veja bem, o código acima funciona, mas eu pretendo adicionar reuso e novas funcionalidades nele. O primeiro passo é separar o suporte a mouse. Crie um novo arquivo js e salve com os outros. chame-o de MouseManager.js
/**
* helper to create dynamically the mousejoint
*
* usage:
*
* var _escala = 25; var world = new b2World(new b2Vec2(0, 0), true); var canvas =
* document.getElementById("c");
*
* //(...)
*
* var mm = new MouseManager({ world : world, canvas : canvas, scale : _escala
* });
*
* //(...)
*
* function update(){ world.Step(1 / 60, 10, 10); world.DrawDebugData();
* world.ClearForces(); mm.step(); }
*
* @param cfg.world
* Box2D world
* @param cfg.canvas
* DOM element to track mouse position
* @param cfg.scale
* pixel to meters proportion used
* @param cfg.camera
* if present, camera will be asked by translation coordinates
*
*/
function MouseManager(cfg) {
var mouseX = undefined;
var mouseY = undefined;
var mousePVec = undefined;
var isMouseDown = undefined;
var selectedBody = undefined;
var mouseJoint = undefined;
var canvasPosition = undefined;
// http://js-tut.aardon.de/js-tut/tutorial/position.html
function getElementPosition(element) {
var elem = element, tagname = "", x = 0, y = 0;
while ((typeof (elem) == "object")
&& (typeof (elem.tagName) != "undefined")) {
y += elem.offsetTop;
x += elem.offsetLeft;
tagname = elem.tagName.toUpperCase();
if (tagname == "BODY")
elem = 0;
if (typeof (elem) == "object") {
if (typeof (elem.offsetParent) == "object")
elem = elem.offsetParent;
}
}
return {
x : x,
y : y
};
}
function handleMouseMove(e) {
var a = 0;
var b = 0;
if (cfg.camera) {
a = cfg.camera.pos.x;
b = cfg.camera.pos.y;
}
mouseX = (e.clientX - canvasPosition.x - a) / cfg.scale;
mouseY = (e.clientY - canvasPosition.y - b) / cfg.scale;
}
function getBodyAtMouse() {
mousePVec = new b2Vec2(mouseX, mouseY);
var aabb = new b2AABB();
aabb.lowerBound.Set(mouseX - 0.001, mouseY - 0.001);
aabb.upperBound.Set(mouseX + 0.001, mouseY + 0.001);
// Query the world for overlapping shapes.
selectedBody = null;
cfg.world.QueryAABB(getBodyCB, aabb);
return selectedBody;
}
function getBodyCB(fixture) {
if (fixture.GetBody().GetType() != b2Body.b2_staticBody) {
if (fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(),
mousePVec)) {
selectedBody = fixture.GetBody();
return false;
}
}
return true;
}
this.step = function() {
if (isMouseDown && (!mouseJoint)) {
var body = getBodyAtMouse();
if (body) {
var md = new b2MouseJointDef();
md.bodyA = world.GetGroundBody();
md.bodyB = body;
md.target.Set(mouseX, mouseY);
md.collideConnected = true;
md.maxForce = 300.0 * body.GetMass();
mouseJoint = cfg.world.CreateJoint(md);
body.SetAwake(true);
}
}
if (mouseJoint) {
if (isMouseDown) {
mouseJoint.SetTarget(new b2Vec2(mouseX, mouseY));
} else {
cfg.world.DestroyJoint(mouseJoint);
mouseJoint = null;
}
}
};
canvasPosition = getElementPosition(cfg.canvas);
document.addEventListener("mousedown", function(e) {
isMouseDown = true;
handleMouseMove(e);
document.addEventListener("mousemove", handleMouseMove, true);
}, true);
document.addEventListener("mouseup", function() {
document.removeEventListener("mousemove", handleMouseMove, true);
isMouseDown = false;
mouseX = undefined;
mouseY = undefined;
}, true);
}
Nada realmente extraordinário foi feito aqui, apenas torno possível reusar o código de mouse que vimos no exemplo original do box2dweb.
Faremos algo similar com o código de debug e de mundo (crie agora o WorldManager.js):
/**
* utilitário para fornecer um meio simples de criar o mundo
*
* @param cfg.world
* instância de b2World para fazermos as simulações
* @param cfg.canvas
* elemento DOM de desenho
* @param cfg.scale
* zoom. Tipo isso
* @param cfg.debug
* se devemos ativar o modo de debug de desenho
* @param cfg.running
* se a simulação começa parada ou não
*/
function WorldManager(cfg) {
function mkFixture(shp, w, h) {
var fixDef = new b2FixtureDef;
if (shp == 0) {
fixDef.shape = new b2CircleShape(w);
} else if (shp == 1) {
fixDef.shape = new b2PolygonShape();
fixDef.shape.SetAsBox(w, h);
}
fixDef.density = 1.0;
fixDef.friction = 0.1;
fixDef.restitution = 0.1;
return fixDef;
}
this.makeCircle = function(x, y, r, isDynamic) {
var fixDef = mkFixture(0, r);
var bodyDef = new b2BodyDef();
bodyDef.type = isDynamic ? b2Body.b2_dynamicBody : b2Body.b2_staticBody;
bodyDef.position.Set(x, y);
var body = cfg.world.CreateBody(bodyDef);
body.CreateFixture(fixDef);
return body;
};
this.makeBox = function(x, y, w, h, isDynamic) {
var fixDef = mkFixture(1, w, h);
var bodyDef = new b2BodyDef();
bodyDef.type = isDynamic ? b2Body.b2_dynamicBody : b2Body.b2_staticBody;
bodyDef.position.Set(x, y);
var body = cfg.world.CreateBody(bodyDef);
body.CreateFixture(fixDef);
return body;
};
this.debugEnabled = function(b) {
var debugDraw = null;
if (b) {
debugDraw = new b2DebugDraw();
// setup debug draw
debugDraw.SetSprite(cfg.canvas.getContext("2d"));
debugDraw.SetDrawScale(cfg.scale);
debugDraw.SetFillAlpha(0.7);
debugDraw.SetLineThickness(0.9);
debugDraw.SetFlags(//
b2DebugDraw.e_shapeBit //
| b2DebugDraw.e_jointBit //
| b2DebugDraw.e_pairBit //
// | b2DebugDraw.e_centerOfMassBit //
| b2DebugDraw.e_aabbBit //
);
}
cfg.world.SetDebugDraw(debugDraw);
};
this.running = function(p) {
cfg.running = p;
};
this.step = function() {
if (cfg.running) {
cfg.world.Step(1 / 60, 10, 10);
cfg.world.DrawDebugData();
cfg.world.ClearForces();
}
};
this.debugEnabled(cfg.debug);
}
Desta vez fizemos, além do encapsulamento, a criação de funções utilitárias. O Box2D não guarda referências paras os objetos do tipo *Def; eles são usados mesmo apenas para não gerarmos uma grande quantidade de parâmetros. Isso significa dizer que este código ainda pode ser melhorado, mas faremos isso... depois, hehe.
Se modificarmos o demo.html e o demo.js para usar estes dois brinquedos, teremos algo desse tipo:
<html>
<head>
Box2dWeb Demo
<script type="text/javascript" src="Box2dWeb-2.1.a.3.min.js"></script>
<script type="text/javascript" src="MouseManager.js"></script>
<script type="text/javascript" src="WorldManager.js"></script>
</head>
<body>
<script type="text/javascript" src="demo.js"></script>
</body>
</html>
var b2Vec2 = Box2D.Common.Math.b2Vec2, b2AABB = Box2D.Collision.b2AABB, b2BodyDef = Box2D.Dynamics.b2BodyDef, b2Body = Box2D.Dynamics.b2Body, b2FixtureDef = Box2D.Dynamics.b2FixtureDef, b2Fixture = Box2D.Dynamics.b2Fixture, b2World = Box2D.Dynamics.b2World, b2MassData = Box2D.Collision.Shapes.b2MassData, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape, b2DebugDraw = Box2D.Dynamics.b2DebugDraw, b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef;
function init() {
var escala = 30;
var world = new b2World(new b2Vec2(0, 10) // gravity
, true // allow sleep
);
var canvas = document.getElementById("canvas");
var wm = new WorldManager({
world : world,
canvas : canvas,
scale : escala,
debug : true,
running : true
});
// mouse
var mm = new MouseManager({
world : world,
canvas : canvas,
scale : escala
});
// ground
wm.makeBox(10, 400 / 30 + 1.8, 20, 2);
wm.makeBox(10, -1.8, 20, 2);
wm.makeBox(-1.8, 13, 2, 14);
wm.makeBox(21.8, 13, 2, 14);
// bodies
for ( var i = 0; i < 10; ++i) {
var a = Math.random() * 10;
var b = Math.random() * 10;
var c = Math.random() + 0.1;
var d = Math.random() + 0.1;
if (Math.random() > 0.5)
wm.makeBox(a, b, c, d, true);
else
wm.makeCircle(a, b, c, true);
}
// update
function update() {
wm.step();
mm.step();
};
window.setInterval(update, 1000 / 60);
};
init();
Ficou diferente né? Agora vamos melhorar as coisas: mude o valor da variável escala para 50. O resultado será os objetos caindo para fora do campo de visão. Isto é um incômodo, usualmente se escolhe um objeto e o seguimos o tempo todo. Para que isso seja possível, precisamos nos meter no ciclo de desenho, fazendo uma translação e desenhando assim a provável área visível. Adicione o CameraManager.js aos outros artefatos:
function CameraManager(cfg) {
var ctx = cfg.canvas.getContext("2d");
this.pos = {
x : 0,
y : 0
};
this.step = function() {
var v = cfg.player.GetPosition();
this.pos.x = -v.x * cfg.scale + cfg.canvas.width / 2;
this.pos.y = -v.y * cfg.scale + cfg.canvas.height / 2;
ctx.translate(this.pos.x, this.pos.y);
};
}
Se você tiver percebido, o MouseManager.js já suporta a existência da câmera, :-) em linhas gerais a posição x,y quer o mouse recuperar deverá contar com a posição que a câmera informar.
Modifique novamente o demo.html e o demo.js:
<html>
<head>
Box2dWeb Demo
<script type="text/javascript" src="Box2dWeb-2.1.a.3.min.js"></script>
<script type="text/javascript" src="MouseManager.js"></script>
<script type="text/javascript" src="WorldManager.js"></script>
<script type="text/javascript" src="CameraManager.js"></script>
</head>
<body>
<script type="text/javascript" src="demo.js"></script>
</body>
</html>
var b2Vec2 = Box2D.Common.Math.b2Vec2, b2AABB = Box2D.Collision.b2AABB, b2BodyDef = Box2D.Dynamics.b2BodyDef, b2Body = Box2D.Dynamics.b2Body, b2FixtureDef = Box2D.Dynamics.b2FixtureDef, b2Fixture = Box2D.Dynamics.b2Fixture, b2World = Box2D.Dynamics.b2World, b2MassData = Box2D.Collision.Shapes.b2MassData, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape, b2DebugDraw = Box2D.Dynamics.b2DebugDraw, b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef;
function init() {
var escala = 50;
var world = new b2World(new b2Vec2(0, 10) // gravity
, true // allow sleep
);
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var wm = new WorldManager({
world : world,
canvas : canvas,
scale : escala,
debug : true,
running : true
});
var jogador = wm.makeBox(7, 7, 0.5, 0.5, true);
var cm = new CameraManager({
player : jogador,
canvas : canvas,
scale : escala
});
// mouse
var mm = new MouseManager({
world : world,
canvas : canvas,
scale : escala,
camera : cm
});
// ground
wm.makeBox(10, 400 / 30 + 1.8, 20, 2);
wm.makeBox(10, -1.8, 20, 2);
wm.makeBox(-1.8, 13, 2, 14);
wm.makeBox(21.8, 13, 2, 14);
// bodies
for ( var i = 0; i < 10; ++i) {
var a = Math.random() * 10;
var b = Math.random() * 10;
var c = Math.random() + 0.1;
var d = Math.random() + 0.1;
if (Math.random() > 0.5)
wm.makeBox(a, b, c, d, true);
else
wm.makeCircle(a, b, c, true);
}
// update
function update() {
ctx.save();
ctx.translate(0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
mm.step();
cm.step();
wm.step();
ctx.restore();
};
window.setInterval(update, 1000 / 60);
};
init();
O resultado é bem interessante:
Update: veja aqui ao vivo: http://jsfiddle.net/sombriks/QKLtC/
Update 2: Acompanhe no github: http://sombriks.github.com/SugarBox2D/
Lembre-se, Box2D faz apenas os Cálculos. Estou usando o modo de desenho de debug aqui. Em outro momento voltamos e conversamos sobre como desenhar coisas interessantes usando as coordenadas que a biblioteca fornece. até lá, experimente a versão java da biblioteca! Ou mesmo a versão C++, :)
Nenhum comentário :
Postar um comentário