Box2D joints: Revolute Joint

Revolute joint дословно переводится как «вращательное соединение», и на самом деле, оно предназначено чтобы тела могли вращаться вокруг точки соединения.

С помощью этого соединения можно создавать цепи и верёвки в играх, а так же объекты, которые могут вращаться сами при помощи особенного свойства вращательных соединений — мотора.

Для примера я реализую следующие объекты:

  • простой прямоугольник прикреплённый к мировому телу, таким образом он будет вращаться как бы в воздухе;
  • 2 обычных объекта (квадрата) с точкой крепления вне своих фигур;
  • молот, который будет бить в землю под действием моторной силы и будет прикреплён к миру;
  • а так же цепь или верёвку (смотря что будет отображаться графикой в вашей игре — физическая суть одинакова).

Управление: P — скрыть\показать профайлер, R — пересоздать сцену, LMB — можно таскать предметы мышью.

Полный код примера
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.Joints.b2RevoluteJoint;
	import Box2D.Dynamics.Joints.b2RevoluteJointDef;
	import Box2D.Dynamics.Joints.b2WeldJointDef;
	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;
	import net.hires.debug.Stats;

	/**
	 * Для статьи "Соединения в 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 VEL_ITERS: int = 10;
		private const POS_ITERS: int = 10;
		
		private var _statsParent: Sprite;
		private var _debugDrawParent: Sprite;
		
		private var _world: b2World;
		private var _mJoint: b2MouseJoint;
		private var _molotJoint: b2RevoluteJoint;
		
		
		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);
			
			// создаём слой для отрисовки физики
			_debugDrawParent = new Sprite();
			addChild(_debugDrawParent);
			
			// создаём слой для отображения профайлера
			_statsParent = new Sprite();
			addChild(_statsParent);
			var stats: Stats = new Stats();
			_statsParent.addChild(stats);
			
			// создаём сцену
			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 
		{
			// создание мира
			_world = new b2World(new b2Vec2(0, 9.8), true);
			// инициализация графики
			initDebugDraw(_debugDrawParent);
			// создание стен
			createWalls(true, true, true);
			// создание объектов
			createObjs(_world);
		}
		
		/**
		 * Полное уничтожение всей сцены
		 */
		private function destroyScene():void 
		{
			destroyMouseJoint();
			_molotJoint = null;
			// чистим отображение
			_debugDrawParent.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);
		}
		
		/**
		 * Создание стен по размеру видимой области
		 * @param	aLeft - создать левую стену?
		 * @param	aRight - создать правую стену?
		 * @param	aTop - создать потолок?
		 */
		private function createWalls(aLeft: Boolean = false, aRight: Boolean = false, aTop: Boolean = false):void 
		{
			var body: b2Body;
			var bDef: b2BodyDef;
			var shape: b2Shape;
			// создание статического прямоугольника. которые будет в роли земли. создаётся по умолчанию
			bDef = new b2BodyDef();
			shape = b2PolygonShape.AsBox(stage.stageWidth * MPP / 2, 20 * MPP / 2);
			bDef.position.Set(stage.stageWidth / 2 * MPP, stage.stageHeight * MPP);
			body = _world.CreateBody(bDef);
			body.CreateFixture2(shape);
			// левая стена
			if (aLeft) {
				bDef = new b2BodyDef();
				shape = b2PolygonShape.AsBox(20 / 2 * MPP, stage.stageHeight / 2 * MPP);
				bDef.position.Set(0, stage.stageHeight / 2 * MPP);
				body = _world.CreateBody(bDef);
				body.CreateFixture2(shape);
			}
			// правая стена
			if (aRight) {
				bDef = new b2BodyDef();
				shape = b2PolygonShape.AsBox(20 * MPP / 2, stage.stageHeight * MPP / 2);
				bDef.position.Set(stage.stageWidth * MPP, stage.stageHeight * MPP / 2);
				body = _world.CreateBody(bDef);
				body.CreateFixture2(shape);
			}
			// потолок
			if (aTop) {
				bDef = new b2BodyDef();
				shape = b2PolygonShape.AsBox(stage.stageWidth / 2 * MPP, 20 / 2 * MPP);
				bDef.position.Set(stage.stageWidth / 2 * MPP, 0 * MPP);
				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;
			
			////////////////////////////////////////////////////////////////////////////
			// крепим прямоугольник к мировому телу
			var a: b2Body = createPhysBox(_world, 350, 100, 60, 20);
			var revDef: b2RevoluteJointDef = new b2RevoluteJointDef();
			revDef.bodyA = _world.GetGroundBody();
			revDef.bodyB = a;
			revDef.localAnchorA = new b2Vec2(500 / 2 * MPP, 60 * MPP);
			revDef.localAnchorB = new b2Vec2();
			_world.CreateJoint(revDef);
			// предадим прямоугольинку чуток угловой скорости, чтобы вертелся для наглядности :)
			a.SetAngularVelocity(2);
			
			////////////////////////////////////////////////////////////////////////////
			// крепим 2 квадрата друг к другу с точкой соединения вне их геометрии
			a = createPhysBox(_world, 250, 200, 30, 30);
			var b: b2Body = createPhysBox(_world, 250, 200, 30, 30);
			revDef.bodyA = a;
			revDef.bodyB = b;
			revDef.localAnchorA = new b2Vec2(40 * MPP, 0 * MPP);
			revDef.localAnchorB = new b2Vec2(-40 * MPP, 0 * MPP);
			_world.CreateJoint(revDef);
			
			////////////////////////////////////////////////////////////////////////////
			// делаем молот
			a = createPhysBox(_world, 100, 100, 100, 20);
			b = createPhysBox(_world, 100, 100, 30, 50);
			// крепим боёк к рукоятке weld joint-ом
			var weldDef: b2WeldJointDef = new b2WeldJointDef();
			weldDef.bodyA = a;
			weldDef.bodyB = b;
			weldDef.localAnchorA = new b2Vec2(50 * MPP, 0);
			weldDef.localAnchorB = new b2Vec2(0, 0);
			weldDef.referenceAngle = 0;
			_world.CreateJoint(weldDef);
			// крепим рукоятку к мировому телу revolute joint-ом
			revDef.bodyA = _world.GetGroundBody();
			revDef.bodyB = a;
			revDef.localAnchorA = new b2Vec2(50 * MPP, 290 * MPP);
			revDef.localAnchorB = new b2Vec2( -45 * MPP, 0);
			// устанавливаем предельный минимальный угол
			revDef.lowerAngle = -Math.PI / 2;
			revDef.upperAngle = 0;
			// включаем применение пределов углов
			revDef.enableLimit = true;
			// включаем мотор, который будет поворачивать молот
			// выставляем момент силы (мощность мотора)
			revDef.maxMotorTorque = 120;
			// угловую скорость
			revDef.motorSpeed = -30;
			revDef.enableMotor = true; // включаем мотор
			_molotJoint = _world.CreateJoint(revDef) as b2RevoluteJoint;
			
			
			////////////////////////////////////////////////////////////////////////////
			// делаем цепь (верёвку)
			// основание в роли круга
			a = createPhysCircle(_world, 400, 100, 20);
			revDef = new b2RevoluteJointDef();
			revDef.bodyA = _world.GetGroundBody();
			revDef.bodyB = a;
			revDef.localAnchorA = new b2Vec2(400 * MPP, 80 * MPP);
			revDef.localAnchorB = new b2Vec2();
			_world.CreateJoint(revDef);
			// звенья
			// первое
			b = createPhysBox(_world, 400, 100, 10, 30);
			revDef.bodyA = a;
			revDef.bodyB = b;
			revDef.localAnchorA = new b2Vec2(0 * MPP, 18 * MPP);
			revDef.localAnchorB = new b2Vec2(0, -14 * MPP);
			_world.CreateJoint(revDef);
			var prevBody: b2Body = b;
			// и ещё 7
			for (var j:int = 0; j < 7; j++) 
			{
				b = createPhysBox(_world, 400, 100 + 30 * (j + 1), 10, 30);
				revDef.bodyA = prevBody;
				revDef.bodyB = b;
				revDef.localAnchorA = new b2Vec2(0, 14 * MPP);
				revDef.localAnchorB = new b2Vec2(0, -14 * MPP);
				_world.CreateJoint(revDef);
				prevBody = b;
			}
			
		}
		
		/**
		 * Создаёт физику круга
		 * @param	aWorld - мир бокса
		 * @param	aX - позиция объекта по X (пикс.)
		 * @param	aY - позиция объекта по Y (пикс.)
		 * @param	aRadius - радиус круга (пикс.)
		 * @param	aDensity - плотность
		 * @param	aFriction - трение
		 * @param	aRestitution - упругость
		 * @return
		 */
		private function createPhysCircle(aWorld: b2World, aX: Number, aY: Number, aRadius: Number, aDensity: Number = 1.0, aFriction: Number = 1.0, aRestitution: Number = 0.5): b2Body
		{
			var bDef: b2BodyDef = new b2BodyDef();
			var fixDef: b2FixtureDef = new b2FixtureDef();
			fixDef.density = aDensity;
    	fixDef.friction = aFriction;
    	fixDef.restitution = aRestitution;
			fixDef.shape = new b2CircleShape(aRadius * MPP);
			
			bDef.position.Set(aX * MPP, aY * MPP);
			bDef.linearDamping = 0.0;
			bDef.angularDamping = 0.0;
			bDef.type = b2Body.b2_dynamicBody;
			
			var b: b2Body = aWorld.CreateBody(bDef);
			b.CreateFixture(fixDef);
			
			return b;
		}
		
		//Create standard boxes of given height , width at x,y
		/**
		 * Создаёт физику прямоугольника
		 * @param	aWorld - мир бокса
		 * @param	aX - позиция по x (пикс.)
		 * @param	aY - позиция по y (пикс.)
		 * @param	aW - длина прямоугольника (пикс.)
		 * @param	aH - высота прямоугольника (пикс.)
		 * @param	aDensity - плотность
		 * @param	aFriction - трение
		 * @param	aRestitution - упругость
		 * @return
		 */
		private function createPhysBox(aWorld: b2World, aX: Number, aY: Number, aW: Number, aH: Number, aDensity: Number = 1.0, aFriction: Number = 1.0, aRestitution: Number = 0.5): b2Body
		{
			var bDef: b2BodyDef = new b2BodyDef();
			var fixDef: b2FixtureDef = new b2FixtureDef();
			fixDef.density = aDensity;
    	fixDef.friction = aFriction;
    	fixDef.restitution = aRestitution;
			var shape: b2PolygonShape = new b2PolygonShape();
			shape.SetAsBox(aW / 2  * MPP, aH / 2  * MPP);
			fixDef.shape = shape;
			
			bDef.position.Set(aX * MPP, aY * MPP);
			bDef.type = b2Body.b2_dynamicBody;
			
			var b: b2Body = aWorld.CreateBody(bDef);
			b.CreateFixture(fixDef);
			
			return b;
		}
		
		/**
		 * Возвращает физ. объект под курсором мыши, если таковой имеется
		 * @param	includeStatic - учитывать статический тела?
		 * @return объект или null
		 */
		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; //возвращаем тело
		}
		
		/**
		 * Уничтожить текущее Mouse соединение
		 */
		private function destroyMouseJoint():void 
		{
			if (_mJoint)
				_world.DestroyJoint(_mJoint); // удаляем соединение
			_mJoint = null;
		}
		
		// обновление физ. мира
		private function updateBox2D(aDTime: Number):void 
		{
			if (_world) {
				_world.Step(aDTime, VEL_ITERS, POS_ITERS);
				_world.ClearForces();
				// рисуем физику с помощью дебаг-рендера
				_world.DrawDebugData();
			}
		}
		
		//} private
		
		
		//////////////////////////////////////////////////////////////////////////////////////////
		//{ EVENTS
		
		private function onKeyDownHandler(e:KeyboardEvent):void 
		{
			switch (e.keyCode) 
			{
				case Keyboard.P:
          _statsParent.visible = !_statsParent.visible;
					break;
					
				case Keyboard.R: // пересоздаём сцену
					destroyScene();
					createScene();
					break;
			}
		}
		
		private function onMouseDown(e:MouseEvent):void 
		{
			// если соединение уже есть, то уничтожаем его
			if (_mJoint)
				destroyMouseJoint();
			// получаем тело под курсором
			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 onMouseUp(e:MouseEvent):void 
		{
			destroyMouseJoint();
		}
		
		private function onEnterFrameHandler(e:Event):void 
		{
			update(1 / stage.frameRate);
		}
		
		//} events
		
		
		//////////////////////////////////////////////////////////////////////////////////////////
		//{ PRIVATE
		
		public function update(aDTime: Number):void
		{
			updateBox2D(aDTime);
			
			// обновляем координаты соединения с мышью, если таковое имеется
			if (_mJoint) {
				var p2:b2Vec2 = new b2Vec2(mouseX * MPP, mouseY * MPP);
				_mJoint.SetTarget(p2); //перемещаем соединение за курсором мыши
			}
			
			// проверяем угол молота
			// и если достигнут предел, то переключаем направление мотора
			if (_molotJoint) {
				if (_molotJoint.GetMotorSpeed() < 0) {
					if (_molotJoint.GetJointAngle() <= _molotJoint.GetLowerLimit())
						_molotJoint.SetMotorSpeed(-_molotJoint.GetMotorSpeed());
				}
				else {
					if (_molotJoint.GetJointAngle() >= _molotJoint.GetUpperLimit())
						_molotJoint.SetMotorSpeed(-_molotJoint.GetMotorSpeed());
				}
			}
			
		}
		
		//} public
		
	}

}

Создание всех объектов заключено в функции createObjs(). В данном примере главное новшество — это пределы углов соединения и мотор.

С пределами углов всё просто: указываем lowerAngle (минимальный угол) и upperAngle (максимальный угол), включаем эту логику выставлением enableLimit = true и угол между соединёнными телами находится в данных пределах.

А вот с мотором поинтереснее. Нужно настраивать 2 параметра — это maxMotorTorque (максимальный крутящий момент силы) и motorSpeed (угловая скорость мотора в рад/сек, которая достигается приложением крутящего момента в точке соединения). Чем больше момент, тем мощнее мотор и тем быстрее достигается указанная скорость мотора. Например, если указать момент = 500 или больше, а скорость = 1 (рад/сек), то тело практически моментально начнёт крутиться со скоростью 1 рад/с по часовой стрелке. А если указывать момент не большой, то тело будет крутиться с постепенным разгоном до указанной скорости. Далее необходимо включить мотор enableMotor = true и «оно задвигалось!».

Разнообразия ради я реализовал изменение скорости мотора, чтобы молот автоматически совершал удары. Вся логика изменения находится в функции update() в строках [461-470]. Один минус — если под молот положить например квадрат, то он стопорится. Всё потому что ожидается достижения предельного угла. Это легко решается, если смену направления силы сделать по таймеру, а не по достижению предельных углов.

Ну и пожалуй самый интересный объект, реализуемый данным соединением, — верёвка. Сначала я реализовал круглое основание и прикрепил его к мировому телу [244-250], затем прикрепил к нему первое звено как частный случай [253-258], а остальные звенья, т.к. они подобны, было легко создавать и прикреплять уже в цикле используя вспомогательную переменную prevBody, которая хранит ссылку на предыдущее звено [259-270].

На этом с данным соединением пожалуй всё, вопросы оставляйте в комментариях!

Скачать исходники