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