quarta-feira, 30 de janeiro de 2013

Simples Implementação de Câmera com Canvas e Box2dweb

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