Weld — крепкое соединение. Как правило оно не может растягиваться или сжиматься (расстояние между телами стремится быть постоянным), а так же тела не могут поворачиваться относительно друг друга (угол между телами стремиться быть постоянным). Получается жёсткое фиксирование тел относительно друг друга с соединением их в определённой точке.
Я продемонстрирую несколько основных видов объектов с применением данного соединения:
- прикрепление обычного динамического тела к мировому телу;
- скрепление 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.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;
/**
* Для статьи "Box2D joints: weld joint" 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;
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();
// чистим отображение
_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;
///////////////////////////////////////////////////////
// создадим 3 одинаковых прямоугольных объекта (квадраты со стороной в 30 пикселей)
// 2 из которых мы соеденим Weld соединением
var a: b2Body = createPhysBox(_world, 350, 100, 30, 30);
var b: b2Body = createPhysBox(_world, 350, 100, 30, 30);
var c: b2Body = createPhysBox(_world, 350, 100, 30, 30);
// создадим Weld joint между b и c
var jDef: b2WeldJointDef = new b2WeldJointDef();
jDef.bodyA = b;
jDef.bodyB = c;
// указываем точку соединения, точка для каждого тела указывается в его локальных координатах
// например если тело - квадрат со стороной 30, то локальная точка (15,15) - это его правый нижний угол.
jDef.localAnchorA = new b2Vec2(-15 * MPP, -15 * MPP);
jDef.localAnchorB = new b2Vec2(15 * MPP, 15 * MPP);
// можно установить угол соединения, расскоментируйте эту строку и посмотрите что изменится
//jDef.referenceAngle = Math.PI / 6; // 30 градусов
// создадим соединение в мире
_world.CreateJoint(jDef);
// теперь соеденим мировое тело с телом a
// можно использовать созданный ранее объект jDef для других тел просто переопределяя тела и нужные совйства
jDef.bodyA = _world.GetGroundBody();
jDef.bodyB = a;
jDef.localAnchorA = new b2Vec2(250 * MPP, 40 * MPP);
jDef.localAnchorB = new b2Vec2();
_world.CreateJoint(jDef);
///////////////////////////////////////////////////////
// создание шестерёнки: 1 круг и 4 прямоугольника
a = createPhysCircle(_world, 100, 100, 60);
b = createPhysBox(_world, 100, 100, 30, 30);
jDef.bodyA = a;
jDef.bodyB = b;
// соединяем круг и прямоугольник, соединяются точки края круга и центра прямоугольника
jDef.localAnchorA = new b2Vec2(60 * MPP, 0);
jDef.localAnchorB = new b2Vec2(0, 0);
_world.CreateJoint(jDef);
// 2-й прямоугольник шестерёнки
b = createPhysBox(_world, 100, 100, 30, 30);
jDef.bodyB = b;
jDef.localAnchorA = new b2Vec2(0, 60 * MPP);
jDef.localAnchorB = new b2Vec2(0, 0);
_world.CreateJoint(jDef);
// 3-й прямоугольник шестерёнки
b = createPhysBox(_world, 100, 100, 30, 30);
jDef.bodyB = b;
jDef.localAnchorA = new b2Vec2(-60 * MPP, 0);
jDef.localAnchorB = new b2Vec2(0, 0);
_world.CreateJoint(jDef);
// 4-й прямоугольник шестерёнки
b = createPhysBox(_world, 100, 100, 30, 30);
jDef.bodyB = b;
jDef.localAnchorA = new b2Vec2(0, -60 * MPP);
jDef.localAnchorB = new b2Vec2(0, 0);
_world.CreateJoint(jDef);
///////////////////////////////////////////////////////
// создание антенны
// 3 первых кусочка
a = createPhysBox(_world, 300, 300, 12, 30);
b = createPhysBox(_world, 300, 300, 10, 30);
c = createPhysBox(_world, 300, 300, 8, 30);
// прикрепляем первый кусочек к телу мира
jDef.bodyA = _world.GetGroundBody();
jDef.bodyB = a;
jDef.localAnchorA = new b2Vec2(250 * MPP, 330 * MPP);
jDef.localAnchorB = new b2Vec2(0, 15 * MPP);
_world.CreateJoint(jDef);
// крепим 2-й кусок к первому
jDef.bodyA = a;
jDef.bodyB = b;
jDef.localAnchorA = new b2Vec2(0, -15 * MPP);
jDef.localAnchorB = new b2Vec2(0, 15 * MPP);
_world.CreateJoint(jDef);
// крепим 3-й кусок ко второму
// локальные координаты крепления остаются те же, поэтому мы не указываем localAnchor-ы по новой
jDef.bodyA = b;
jDef.bodyB = c;
_world.CreateJoint(jDef);
// 4-й кусочек
a = createPhysBox(_world, 300, 300, 5, 30);
jDef.bodyA = c;
jDef.bodyB = a;
_world.CreateJoint(jDef);
}
/**
* Создаёт физику круга
* @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); //перемещаем соединение за курсором мыши
}
}
//} public
}
}
Всё самое важное и новое для нас находится в функции createObjs(), в ней создаются все объекты и соединения.
На данном этапе нужно хорошо понять 2 вещи: относительность точки соединения и угла соединения.
На Рисунке 1 представлена наглядность для понимания координат точки соединения localAnchor.

Теперь разберёмся с углом соединения referenceAngle. Если мы укажем угол соединения следующим образом:
jDef.referenceAngle = Math.PI / 6; // 30 градусов
то угол между телами станет = 30 градусов. Т.е. если первое тело будет статично (как в случае соединения мира и тела «a» в примере), то второе тело будет повернуто по часовой стрелке на 30 градусов.
Обратите внимание, что ось Y в системе координат box2d направлена вниз, поэтому поворот на положительный угол будет совершаться по часовой стрелке, а отрицательный угол — против часовой стрелки.
На Рисунке 2 схематично отображено соединение с углами 45, -135, 270 градусов.
