HTML5 + Box2d. Подключаем физический движок.



Некоторые из вас возможны знакомы с самим физическим движком Box2d, который портирован на различные платформы и стал особенно популярный на платформе Flash. Так вот, Box2d отныне портирован и на JS, но по правде сказать это автоматический конверт с Box2DFlashAS3_1.4.3.1.

Вкратце, Box2d — это двухмерный физический движок, с помощью его мы и попробует сделать игру на HTML5. Все исходники и примеры доступны для скачивания:
Box2D JS library
oncatenated version (~350KB)
minified version (~170 KB)
Box2d JS 2.0 экспериментальная

А теперь давайте аккуратно ступая, пройдемся по шагам. Чтобы избежать вот таких вот инклюдов:

<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]--> 
<script src="lib/prototype-1.6.0.2.js"></script> 
 
<!-- box2djs --> 
<script src='js/box2d/common/b2Settings.js'></script> 
<script src='js/box2d/common/math/b2Vec2.js'></script> 
<script src='js/box2d/common/math/b2Mat22.js'></script> 
<script src='js/box2d/common/math/b2Math.js'></script> 
<script src='js/box2d/collision/b2AABB.js'></script> 
<script src='js/box2d/collision/b2Bound.js'></script> 
<script src='js/box2d/collision/b2BoundValues.js'></script> 
<script src='js/box2d/collision/b2Pair.js'></script> 
<script src='js/box2d/collision/b2PairCallback.js'></script> 
<script src='js/box2d/collision/b2BufferedPair.js'></script> 
<script src='js/box2d/collision/b2PairManager.js'></script> 
<script src='js/box2d/collision/b2BroadPhase.js'></script> 
<script src='js/box2d/collision/b2Collision.js'></script> 
<script src='js/box2d/collision/Features.js'></script> 
<script src='js/box2d/collision/b2ContactID.js'></script> 
<script src='js/box2d/collision/b2ContactPoint.js'></script> 
<script src='js/box2d/collision/b2Distance.js'></script> 
<script src='js/box2d/collision/b2Manifold.js'></script> 
<script src='js/box2d/collision/b2OBB.js'></script> 
<script src='js/box2d/collision/b2Proxy.js'></script> 
<script src='js/box2d/collision/ClipVertex.js'></script> 
<script src='js/box2d/collision/shapes/b2Shape.js'></script> 
<script src='js/box2d/collision/shapes/b2ShapeDef.js'></script> 
<script src='js/box2d/collision/shapes/b2BoxDef.js'></script> 
<script src='js/box2d/collision/shapes/b2CircleDef.js'></script> 
<script src='js/box2d/collision/shapes/b2CircleShape.js'></script> 
<script src='js/box2d/collision/shapes/b2MassData.js'></script> 
<script src='js/box2d/collision/shapes/b2PolyDef.js'></script> 
<script src='js/box2d/collision/shapes/b2PolyShape.js'></script> 
<script src='js/box2d/dynamics/b2Body.js'></script> 
<script src='js/box2d/dynamics/b2BodyDef.js'></script> 
<script src='js/box2d/dynamics/b2CollisionFilter.js'></script> 
<script src='js/box2d/dynamics/b2Island.js'></script> 
<script src='js/box2d/dynamics/b2TimeStep.js'></script> 
<script src='js/box2d/dynamics/contacts/b2ContactNode.js'></script> 
<script src='js/box2d/dynamics/contacts/b2Contact.js'></script> 
<script src='js/box2d/dynamics/contacts/b2ContactConstraint.js'></script> 
<script src='js/box2d/dynamics/contacts/b2ContactConstraintPoint.js'></script> 
<script src='js/box2d/dynamics/contacts/b2ContactRegister.js'></script> 
<script src='js/box2d/dynamics/contacts/b2ContactSolver.js'></script> 
<script src='js/box2d/dynamics/contacts/b2CircleContact.js'></script> 
<script src='js/box2d/dynamics/contacts/b2Conservative.js'></script> 
<script src='js/box2d/dynamics/contacts/b2NullContact.js'></script> 
<script src='js/box2d/dynamics/contacts/b2PolyAndCircleContact.js'></script> 
<script src='js/box2d/dynamics/contacts/b2PolyContact.js'></script> 
<script src='js/box2d/dynamics/b2ContactManager.js'></script> 
<script src='js/box2d/dynamics/b2World.js'></script> 
<script src='js/box2d/dynamics/b2WorldListener.js'></script> 
<script src='js/box2d/dynamics/joints/b2JointNode.js'></script> 
<script src='js/box2d/dynamics/joints/b2Joint.js'></script> 
<script src='js/box2d/dynamics/joints/b2JointDef.js'></script> 
<script src='js/box2d/dynamics/joints/b2DistanceJoint.js'></script> 
<script src='js/box2d/dynamics/joints/b2DistanceJointDef.js'></script> 
<script src='js/box2d/dynamics/joints/b2Jacobian.js'></script> 
<script src='js/box2d/dynamics/joints/b2GearJoint.js'></script> 
<script src='js/box2d/dynamics/joints/b2GearJointDef.js'></script> 
<script src='js/box2d/dynamics/joints/b2MouseJoint.js'></script> 
<script src='js/box2d/dynamics/joints/b2MouseJointDef.js'></script> 
<script src='js/box2d/dynamics/joints/b2PrismaticJoint.js'></script> 
<script src='js/box2d/dynamics/joints/b2PrismaticJointDef.js'></script> 
<script src='js/box2d/dynamics/joints/b2PulleyJoint.js'></script> 
<script src='js/box2d/dynamics/joints/b2PulleyJointDef.js'></script> 
<script src='js/box2d/dynamics/joints/b2RevoluteJoint.js'></script> 
<script src='js/box2d/dynamics/joints/b2RevoluteJointDef.js'></script>

Я специально выпустил mini-версию движка, с ней инклюды сводяться к:

<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]--> 
<script src="lib/prototype-1.6.0.2.js"></script> 
<script src="box2djs.min.js"></script>

Для этого вам понадобятся Prototype и excanvas(включен в дистрибутив). Но используя мой минимизированный Box2d в одном файле вы сможете начать быстро и код будет выглядеть чище.

Итак, давайте начнем:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<title>SavageLook.com - Box2D JS Hello World</title>
	<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
	<script src="lib/prototype-1.6.0.2.js"></script>
	<script src="box2djs.min.js"></script>

Начали мы с того что подключили обязательные js библиотеки, чтобы заработал Box2d. Excanvas требуется для устранения неприятностей с версиями IE, которые не поддерживают html5 canvas. Так же мы подключили Prototype Javascript Framework и минимизированный box2d. Давайте теперь применим физику к canvas элементам:

<script type="text/javascript">
	var world;
	var ctx;
	var canvasWidth;
	var canvasHeight;
	var canvasTop;
	var canvasLeft;
 
	function drawWorld(world, context) {
		for (var j = world.m_jointList; j; j = j.m_next) {
			drawJoint(j, context);
		}
		for (var b = world.m_bodyList; b; b = b.m_next) {
			for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
				drawShape(s, context);
			}
		}
 
		ctx.font = 'bold 18px arial';
		ctx.textAlign = 'center';
		ctx.fillStyle = '#000000';
		ctx.fillText("Click the screen to add more objects", 400, 20);
		ctx.font = 'bold 14px arial';
		ctx.fillText("Performance will vary by browser", 400, 40);
 
	}

Мы объявили глобальные переменные, которые определяют понятие World. Так же у нас есть функция «drawWorld» которая будет, как следует из названия, рисовать формы и соединения, которые составляют мир Box2D JS. Каждый элемент отрисовывается в индивидуальном порядке и добавляется в World с помощью функции «createBody». Важно отметить что в случае моей демострации «drawWorld()» будет вызываться с каждым шагом. Думайте о canvas, как о по кадровой анимации и каждый вызов drawWorld () является одним кадром. Flash девелоперы думаю поняли о чем я говорю.

function drawJoint(joint, context) {
		var b1 = joint.m_body1;
		var b2 = joint.m_body2;
		var x1 = b1.m_position;
		var x2 = b2.m_position;
		var p1 = joint.GetAnchor1();
		var p2 = joint.GetAnchor2();
		context.strokeStyle = '#00eeee';
		context.beginPath();
		switch (joint.m_type) {
		case b2Joint.e_distanceJoint:
			context.moveTo(p1.x, p1.y);
			context.lineTo(p2.x, p2.y);
			break;
 
		case b2Joint.e_pulleyJoint:
			// TODO
			break;
 
		default:
			if (b1 == world.m_groundBody) {
				context.moveTo(p1.x, p1.y);
				context.lineTo(x2.x, x2.y);
			}
			else if (b2 == world.m_groundBody) {
				context.moveTo(p1.x, p1.y);
				context.lineTo(x1.x, x1.y);
			}
			else {
				context.moveTo(x1.x, x1.y);
				context.lineTo(p1.x, p1.y);
				context.lineTo(x2.x, x2.y);
				context.lineTo(p2.x, p2.y);
			}
			break;
		}
		context.stroke();
	}
 
	function drawShape(shape, context) {
		context.strokeStyle = '#ffffff';
		if (shape.density == 1.0) {
			context.fillStyle = "red";
		} else {
			context.fillStyle = "black";
		}
		context.beginPath();
		switch (shape.m_type) {
		case b2Shape.e_circleShape:
			{
				var circle = shape;
				var pos = circle.m_position;
				var r = circle.m_radius;
				var segments = 16.0;
				var theta = 0.0;
				var dtheta = 2.0 * Math.PI / segments;
 
				// draw circle
				context.moveTo(pos.x + r, pos.y);
				for (var i = 0; i < segments; i++) {
					var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
					var v = b2Math.AddVV(pos, d);
					context.lineTo(v.x, v.y);
					theta += dtheta;
				}
				context.lineTo(pos.x + r, pos.y);
 
				// draw radius
				context.moveTo(pos.x, pos.y);
				var ax = circle.m_R.col1;
				var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
				context.lineTo(pos2.x, pos2.y);
			}
			break;
		case b2Shape.e_polyShape:
			{
				var poly = shape;
				var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
				context.moveTo(tV.x, tV.y);
				for (var i = 0; i < poly.m_vertexCount; i++) {
					var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
					context.lineTo(v.x, v.y);
				}
				context.lineTo(tV.x, tV.y);
			}
			break;
		}
		context.fill();
		context.stroke();
	}

Чуть выше функции рисования body: drawJoints() и drawShape(). Я не буду здесь останавливаться подробнее но скажу что эти функции позволяет наделить body визуальной оболочкой. Эти функции вызывают canvas context 2D drawing API чтобы создать формы падающих прямоугольников. Это самый простой случай и не требующий внешних зависимостей. Хороший пример:



function createWorld() {
		var worldAABB = new b2AABB();
		worldAABB.minVertex.Set(-1000, -1000);
		worldAABB.maxVertex.Set(1000, 1000);
		var gravity = new b2Vec2(0, 300);
		var doSleep = true;
		world = new b2World(worldAABB, gravity, doSleep);
		createGround(world);
		return world;
	}

Вот где создается физика «мира»(World) Box2D JS. Мы определяем границы нашего AABB мира — minVertex и maxVertex. И задаем ускорение свободного падения, гравитацию, а после этого нам нужно определить землю(плоскость) и задать ей свойства:


	function createGround(world) {
		var groundSd = new b2BoxDef();
		groundSd.extents.Set(400, 30);
		groundSd.restitution = 0.0;
		var groundBd = new b2BodyDef();
		groundBd.AddShape(groundSd);
		groundBd.position.Set(400, 470);
		return world.CreateBody(groundBd);
	}
 
	function createBall(world, x, y) {
		var ballSd = new b2CircleDef();
		ballSd.density = 1.0;
		ballSd.radius = 20;
		ballSd.restitution = 0.5;
		ballSd.friction = 0.5;
		var ballBd = new b2BodyDef();
		ballBd.AddShape(ballSd);
		ballBd.position.Set(x,y);
		return world.CreateBody(ballBd);
	}
 
        function createBox(world, x, y, width, height, fixed) {
		if (typeof(fixed) == 'undefined') fixed = true;
		var boxSd = new b2BoxDef();
		if (!fixed) boxSd.density = 1.0; 
		boxSd.restitution = 0.0;
		boxSd.friction = 1.0;
		boxSd.extents.Set(width, height);
		var boxBd = new b2BodyDef();
		boxBd.AddShape(boxSd);
		boxBd.position.Set(x,y);
		return world.CreateBody(boxBd);
	}

Мы создали функции createGround, createBall и createBox. Давайте уточним здесь некоторые вещи. Для каждого body мы определяем его shape defenition, у которого в свою очередь есть ряд свойств, которые определяют его поведение в нашем AABB мире(оф. доки box2d на русском, осторожно — доки под разные версии box2d!) — трение, плотность и прочее.

Так же стоит упомнить о задании размеров в двумерном пространстве Box2d — (x,y), где x и y — это расстояние от угла шейпа до его центра. Например так правильно задать шейп 100х100: shapeDef.extents.Set(50,50).

Shape defenition используется чтобы задать body defenition(в новой, не портированной версии box2d 2.1 все немного проще). После этого body по всем канонам AABB мира официально допускается в мир. Местоположение body определяет его центр, а не левый верхний угол. И с помощью функции CreateBody() наше тело добавляет в наш мир:

function createHelloWorld() {
		// H
		createBox(world, 50, 420, 10, 20, false);
		createBox(world, 90, 420, 10, 20, false);
		createBox(world, 70, 395, 30, 5, false);
		createBox(world, 50, 370, 10, 20, false);
		createBox(world, 90, 370, 10, 20, false);
 
		// E
		createBox(world, 140, 435, 30, 5, false);
		createBox(world, 120, 420, 10, 10, false);
		createBox(world, 130, 405, 20, 5, false);
		createBox(world, 120, 390, 10, 10, false);
		createBox(world, 140, 375, 30, 5, true);
 
		// L
		createBox(world, 200, 435, 20, 5, false);
		createBox(world, 185, 400, 5, 30, false);
 
		// L
		createBox(world, 250, 435, 20, 5, false);
		createBox(world, 235, 400, 5, 30, false);
 
		// O
		createBox(world, 300, 435, 20, 5, false);
		createBox(world, 285, 405, 5, 25, false);
		createBox(world, 315, 405, 5, 25, false);
		createBox(world, 300, 375, 20, 5, false);
 
		// W
		createBox(world, 390, 435, 40, 5, false);
		createBox(world, 360, 390, 10, 40, false);
		createBox(world, 420, 390, 10, 40, false);
		createBox(world, 390, 415, 5, 15, false);
 
		// O
		createBox(world, 460, 435, 20, 5, false);
		createBox(world, 445, 405, 5, 25, false);
		createBox(world, 475, 405, 5, 25, false);
		createBox(world, 460, 375, 20, 5, false);
 
		// R
		createBox(world, 495, 410, 5, 30, false);
		createBox(world, 518, 425, 5, 15, false);
		createBox(world, 515, 405, 15, 5, false);
		createBox(world, 525, 390, 5, 10, false);
		createBox(world, 510, 375, 20, 5, false);
 
		// L
		createBox(world, 560, 435, 20, 5, false);
		createBox(world, 545, 400, 5, 30, false);
 
		// D
		createBox(world, 610, 435, 20, 5, false);
		createBox(world, 595, 405, 5, 25, false);
		createBox(world, 625, 405, 5, 25, false);
		createBox(world, 610, 375, 20, 5, false);
 
		// !
		createBox(world, 650, 430, 10, 10, false);
		createBox(world, 650, 380, 10, 40, false);
	}

Так выглядит надпись «Hello World», составленная из тел(прямоугольников). Кстате я сжульничал с буквой «E», немного изменив физику. Ведь я же бог этого маленького мира.

function step(cnt) {
		var stepping = false;
		var timeStep = 1.0/60;
		var iteration = 1;
		world.Step(timeStep, iteration);
		ctx.clearRect(0, 0, canvasWidth, canvasHeight);
		drawWorld(world, ctx);
		setTimeout('step(' + (cnt || 0) + ')', 10);
	}

Именна функция step() и заставляет все работать. Она вызывается через определенный промежуток времени, чтобы создать иллюзию анимации нашего мира, основанного на canvas. Чем больше раз в секунду она вызывается — тем плавнее и больше нагрузка на компьютер. Это аналог fps, во флеше это ENTER_FRAME ивент.

// main entry point
	Event.observe(window, 'load', function() {
		world = createWorld();
		ctx = $('canvas').getContext('2d');
		var canvasElm = $('canvas');
		canvasWidth = parseInt(canvasElm.width);
		canvasHeight = parseInt(canvasElm.height);
		canvasTop = parseInt(canvasElm.style.top);
		canvasLeft = parseInt(canvasElm.style.left);
 
		createHelloWorld();
 
		Event.observe('canvas', 'click', function(e) {
				if (Math.random() > 0.5) {
					//createBox(world, Event.pointerX(e), Event.pointerY(e), 10, 10, false);
					createBox(world, e.clientX, e.clientY, 10, 10, false);
				} else {
					createBall(world, Event.pointerX(e), Event.pointerY(e));
				}
		});
		step();
	});
	</script>

Итак, мы использовали Prototype event handling чтобы дождаться пока загрузиться страничка и начнет загружаться наш исходный код. Сперва мы создали наш Box2d JS world и определили пространственную ориентацию canvas. После этого мы составили из несколько десятков body надпись «Hello World». Наконец мы добавили слушатель событий, а именно повесили триггер на клик мыши, по выполнению которого в наш мир добавляется падающее тело. И вот по первому же исполнению функции step() все ожило.

</head>
    <body style="margin:0px;">
        <canvas id="canvas" width='800' height='500' style="background-color:#eeeeee;"></canvas>
    </body>
 </html>

Добавить canvas контейнер, где все и будет происходить. Осталось напомнить вам, что в IE, версии ниже 9 вы ниче не увидите без excanvas.

Результат
Пример реализации

Оригинал статьи
  • avatar
  • +1

6 комментариев

avatar
Thanks for the post. I hope you find this article helpful.

— Tony, SavageLook.com
avatar
Check this. Newer box2d version. Can u release new one and mini it?
avatar
I did check it out and he already made a compiled, mini'ed version: www.html5dev.ru/gamedev/2010/11/30/html5-box2d-podklyuchaem-fizicheskiy-dvizhok.html

Unfortunately, it doesn't work for me. I don't think its entirely stable yet.
avatar
Anyway thanks for your version, im using it. Hoping for port of box2dflash2.1 versions on future
avatar
Жаль не особо много фпс выдает, жду Impact JS Engine с соседнего топика
avatar
github.com/jadell/box2dnode — box2d under nodeJs :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.