Перемещение объектов мышкой в Box2D Flash

Всем привет! В этой статье я расскажу как таскать физические объекты в Box2D мышкой.

И делать мы это будем во Flash.

Цель

Сразу покажу результат, который у нас должен получиться, чтобы навести интригу и подкормить интерес)

Можно зажимать ЛКМ на объектах и таскать их, а когда вы отожмёте кнопку, то объект отцепляется от мыши:

Реализация

За основу возьмём проект из статьи Основы Box2D и дополним его новым функционалом.

Вся идея состоит в том, что при нажатии мыши мы создаём специальное соединение (joint) средствами физического движка между мышью и мировым телом. А затем, при дальнейшем движении мыши, мы обновляем точку крепления к мировому телу исходя из координат мыши на нём.

package 
{
	import Box2D.Collision.b2AABB;
	import Box2D.Collision.Shapes.b2CircleShape;
	import Box2D.Collision.Shapes.b2PolygonShape;
	import Box2D.Collision.Shapes.b2Shape;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2DebugDraw;
	import Box2D.Dynamics.b2Fixture;
	import Box2D.Dynamics.b2FixtureDef;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.Joints.b2MouseJoint;
	import Box2D.Dynamics.Joints.b2MouseJointDef;
	import flash.display.DisplayObjectContainer;
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.ui.Keyboard;

	/**
	 * Для статьи "Перемещение объектов мышкой в Box2DFlash" cyber-code.ru
	 * @author Monax
	 */
	[Frame(factoryClass="Preloader")]
	public class Main extends Sprite 
	{
		private const PPM: Number = 30; // pixel per meter - множитель для перевода метров в пиксели
		private const MPP: Number = 1 / 30; // meter per pixel - для перевода пикселей в метры
		private const VelIters: int = 10;
		private const PosIters: int = 10;
		private var _world: b2World;
		
		private var _mJoint: b2MouseJoint;
		
		
		public function Main():void 
		{
			if (stage) init() else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		
		//////////////////////////////////////////////////////////////////////////////////////////
		//{ PRIVATE
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			
			createScene();
			addEventListener(Event.ENTER_FRAME, onEnterFrameHandler);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDownHandler);
			
			stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
			stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
		}
		
		// создание всей сцены
		private function createScene():void 
		{
			// создание мира
			var gv: b2Vec2 = new b2Vec2(0, 9.8);
			_world = new b2World(gv, true);
			// инициализация графики
			initDebugDraw(this);
			// создание объектов
			createWalls();
			createObjs(_world);
		}
		
		// полное уничтожение всей сцены
		private function destroyScene():void 
		{
			// уничтожаем отображение
			removeChildren();
			// уничтожаем все тела в мире
			if (_world)
				for (var body: b2Body = _world.GetBodyList(); body; body = body.GetNext()) 
					if (body) _world.DestroyBody(body);
			// обнуляем переменную мира
			_world = null;
		}
		
		/**
		 * Инициализация debug-отрисовки
		 */
		private function initDebugDraw(aDDParent: DisplayObjectContainer):void 
		{
			// создаём специальный фон для дебаг отрисовки
			// это необходимо, чтобы на этапе дебаг отрисовки срабатывали клики мыши например
			var ddFon: Sprite = new Sprite();
			var gr: Graphics = ddFon.graphics;
			gr.lineStyle(0);
			gr.beginFill(0x333333, 1);
			gr.drawRect(-1000, -1000, 2000, 2000);
			aDDParent.addChild(ddFon);
			var ddDummy: Sprite = new Sprite();
			aDDParent.addChild(ddDummy);
			var debugDraw: b2DebugDraw = new b2DebugDraw();
			debugDraw.SetSprite(ddDummy);
			debugDraw.SetDrawScale(PPM);
			// указываем флаги - что необходимо отрисовывать
			debugDraw.SetFlags(b2DebugDraw.e_controllerBit + b2DebugDraw.e_pairBit + b2DebugDraw.e_shapeBit + b2DebugDraw.e_jointBit);
			debugDraw.SetFillAlpha(0.5);
			_world.SetDebugDraw(debugDraw);
		}
		
		// создание стен
		private function createWalls():void 
		{
			var body: b2Body;
			var bDef: b2BodyDef;
			var shape: b2Shape;
			// создание статического прямоугольника. которые будет в роли земли
			bDef = new b2BodyDef();
			shape = b2PolygonShape.AsBox(640 * MPP / 2, 20 * MPP / 2);
			bDef.position.Set(640 / 2 * MPP, 480 * MPP);
			body = _world.CreateBody(bDef);
			body.CreateFixture2(shape);
			// левая стена
			bDef = new b2BodyDef();
			shape = b2PolygonShape.AsBox(20 * MPP / 2, 480 * MPP / 2);
			bDef.position.Set(0, 480 * MPP / 2);
			body = _world.CreateBody(bDef);
			body.CreateFixture2(shape);
			// правая стена
			bDef = new b2BodyDef();
			shape = b2PolygonShape.AsBox(20 * MPP / 2, 480 * MPP / 2);
			bDef.position.Set(640 * MPP, 480 * MPP / 2);
			body = _world.CreateBody(bDef);
			body.CreateFixture2(shape);
		}
		
		// создание различных объектов
		private function createObjs(aWorld: b2World):void 
		{
			var i: int;
			var body: b2Body;
			var bDef: b2BodyDef;
			var fixDef: b2FixtureDef;
			// создаём круги
			for (i = 0; i < 10; i++) {
				bDef = new b2BodyDef();
				bDef.type = b2Body.b2_dynamicBody;
				bDef.position.Set(Math.random() * 640 * MPP, Math.random() * 480 * MPP);
				body = aWorld.CreateBody(bDef);
				fixDef = new b2FixtureDef();
				fixDef.shape = new b2CircleShape(Math.random() + 0.2);
				fixDef.density = Math.random() + 0.2;
				body.CreateFixture(fixDef);
			}
			// создаём прямоугольники
			for (i = 0; i < 10; i++) {
				bDef = new b2BodyDef();
				bDef.type = b2Body.b2_dynamicBody;
				bDef.position.Set(Math.random() * 640 * MPP, Math.random() * 480 * MPP);
				body = aWorld.CreateBody(bDef);
				fixDef = new b2FixtureDef();
				fixDef.shape = b2PolygonShape.AsBox(Math.random() + 0.5, Math.random() + 0.4);
				fixDef.density = Math.random() + 0.2;
				body.CreateFixture(fixDef);
			}
		}
		
		// обновление мира
		private function updateBox2D(aDTime: Number):void 
		{
			if (_world) {
				_world.Step(aDTime, VelIters, PosIters);
				_world.ClearForces();
				// дебаг рендер физики
				_world.DrawDebugData();
			}
		}
		
		private function GetBodyAtMouse(includeStatic:Boolean = false): b2Body
		{
			var mXWorldPhys: Number = mouseX * MPP;
			var mYWorldPhys: Number = mouseY * MPP;
			var mousePVec: b2Vec2 = new b2Vec2(mXWorldPhys, mYWorldPhys);//записываем текущие координаты курсора
			var aabb: b2AABB = new b2AABB(); // создаем прямоугольную область
			aabb.lowerBound.Set(mXWorldPhys - 0.001, mYWorldPhys - 0.001); //вокруг курсора мыши
			aabb.upperBound.Set(mXWorldPhys + 0.001, mYWorldPhys + 0.001);
			var body: b2Body = null;
			var fixture: b2Fixture;
			function GetBodyCallback(fixture:b2Fixture): Boolean 
			{
				var shape:b2Shape = fixture.GetShape(); //получаем шейп который находится под курсором
				// если тело не статическое
				if (fixture.GetBody().GetType() != b2Body.b2_staticBody || includeStatic) { 
					// проверяем находится ли точка-позиция курсора в рамках тела
					var inside: Boolean = shape.TestPoint(fixture.GetBody().GetTransform(), mousePVec);
					// если да
					if (inside) {
						body = fixture.GetBody(); //получаем ссылку на тело
						return false;
					}
				}
				return true;
			}
			_world.QueryAABB(GetBodyCallback, aabb); //проверяем на попадание любых тел в область aabb
			return body; //возвращаем тело
		}
		
		//} private
		
		
		//////////////////////////////////////////////////////////////////////////////////////////
		//{ EVENTS
		
		private function onKeyDownHandler(e:KeyboardEvent):void 
		{
			switch (e.keyCode) 
			{
				case Keyboard.R: // пересоздаём сцену
						destroyScene();
						createScene();
						break;
			}
		}
		
		private function onMouseDown(e:MouseEvent):void 
		{
			// если соединение уже есть, то уничтожаем его
			if (_mJoint)
				destroyCurrentJoint();
			// получаем тело под курсором
			var body:b2Body = GetBodyAtMouse();
			if (body) {
				var md:b2MouseJointDef = new b2MouseJointDef(); //создаем настройки соединения
				md.bodyA = _world.GetGroundBody(); //один конец крепим к миру
				md.bodyB = body; //другой к телу
				md.target.Set(mouseX * MPP, mouseY * MPP); //соединение создается от курсора мыши
				md.collideConnected = true;
				md.maxForce = 3000; //макс. сила которая может быть приложена к телу
				_mJoint = _world.CreateJoint(md) as b2MouseJoint; //создаем соединение
				body.SetAwake(true); //будим тело
			}
		}
		
		private function destroyCurrentJoint():void 
		{
			if (_mJoint)
				_world.DestroyJoint(_mJoint); // удаляем соединение
			_mJoint = null;
		}
		
		private function onMouseUp(e:MouseEvent):void 
		{
			destroyCurrentJoint();
		}
		
		private function onEnterFrameHandler(e:Event):void 
		{
			updateBox2D(1 / stage.frameRate);
			
			if (_mJoint) { //если кнопка мыши нажата и соединение уже существует
				var p2:b2Vec2 = new b2Vec2(mouseX * MPP, mouseY * MPP);
				_mJoint.SetTarget(p2); //перемещаем соединение за курсором мыши
			}
		}
		
		//} events
		
	}

}

Строка 37: Для соединения с мышкой существует специальный класс b2MouseJoint. Объявляем нашу переменную соединения в переменных класса.

Строки 179-206: Специальная функция GetBodyAtMouse для определения физического тела под курсором мыши. Она нам понадобится в момент нажатия мыши.

Строки 214-242: Создание соединения мы производим в событии MOUSE_DOWN, а уничтожение соединения в MOUSE_UP.

Строки 244-249: Специальная функция уничтожения соединения. Используется в несколькоих местах, чтобы не случилось такого, что старое соединение осталось и создалось новое.

Строки 260-263: Не забываем обновлять координаты крепления соединения (если оно создано) относительно позиции курсора мыши.

Вот и все дела!

Материалы

Исходник можно скачать тут: box2dMouseJoint