Новый nape. Соединение тел.

Новый nape:
1. Новый nape. Hello world
2. Новый nape. Обработка событий
3. Новый nape. Testbed. И немного теории
4. Новый nape. Создание тел, материалы, фильтрация столкновений
5. Новый nape. Соединение тел (Его вы сейчас читаете)
6. Новый nape. Приложение сил к телам и raycast

Все примеры кода в Testbed: Скачать проект для FlashDevelop.

По сравнению, с той версией, что использовалась при написании первой части, были исправлены всякие ошибки в nape, в том числе касающиеся темы данной статьи, при написании примеров к этой статья я использовал версию m6.1 r61.

Соединения тел

Соединения тел в nape называются Constraint, более правильно это перевести как ограничение взаимного движения тел, но я буду все равно называть их соединениями. В nape есть следующие виды соединений:
  • PivotJoint — самое привычное соединение, два тела соединяются так, что две точки на них всегда совпадают
  • DistanceJoint — расстояние между двумя заданными точками двух тел поддерживается в заданных пределах
  • LineJoint — точка на одном теле движется по отрезку на другом теле
  • WeldJoint — два тела движутся полностью вместе, как если бы все их фигуры добавили к одному телу
  • AngleJoint — ограничивает угол поворота одного тела относительного другого в заданных пределах. Тела при этом могут разлететься как угодно далеко, так что обычно стоит использовать вместе с PivotJoint. Кроме того можно задавать соотношение углов поворота: это может пригодится, например, при создании чего-то вроде шестеренок.
  • MotorJoint — задает соотношение скоростей вращения двух тел. При определенных параметрах может использоваться как мотор. Также имеет смысл использовать вместе с PivotJoint.

Картинка для ясности

PivotJoint и общие для всех соединений свойства

PivotJoint — самое простое соединение, и, наверное, самое востребованное. Тела соединяются так, как будто их свинтили винтом, и они теперь вокруг этого винта крутятся. Для создания PivotJoint нужно указать два тела и точки на этих телах, которые должны всегда совпадать.

var pivotJointWorldPoint:Vec2 = new Vec2(150, 150);
pivotJoint = new PivotJoint(pivotJointBody1, pivotJointBody2, 
        pivotJointBody1.worldToLocal(pivotJointWorldPoint), pivotJointBody2.worldToLocal(pivotJointWorldPoint));

В качестве параметра конструктора передаются точки в локальных координатах тела. Но во-первых после вызова align мы точно не знаем, какие у нас локальные координаты, а во-вторых обычно надо добавить соединение тела, условие которого уже выполняется, поэтому я задал точку в глобальных координатах и воспользовался функцией worldToLocal для перевода.

Если нужно привязать тело к «земле», то в качестве второго тела можно использовать любое статическое тело. В классе Space есть свойство world, которое является таким заранее созданным статическим телом.
var joint1Pos:Vec2 = new Vec2(0, -50); 
space.constraints.add(joint1 = new PivotJoint(space.world, body1, body1.localToWorld(joint1Pos), joint1Pos));

Здесь я наоборот задал локальные координаты в теле body1. Локальная система координат тела world совпадает с глобальной.

У всех ограничений есть интересное свойство ignore:
pivotJoint.ignore = true;

Оно делает так, что столкновения между соединенными телами не учитываются. Часто это бывает полезно.

Иногда бывает нужно временно отключить соединение. Можно удалить соединение из мира, а можно просто установить свойство active в значение false.
pivotJoint.space = null; //Можно так
pivotJoint.active = false; // Но так лучше, если потом потребуется включить соединение обратно


Кроме того все типы соединений имеют свойство stiff. На русский это можно перевести как «жесткое соединение».

pivotJoint.stiff = false;

Разница в поведении такова: если соединение жесткое, то каждый шаг nape будет устранять ошибку соединения (т.е. разницу между текущим положением тел и положением тел, при котором условие выполняется) перемещая тела, если же соединение не жесткое, то к телам будут прикладываться силы необходимые для приведения тел в нужное положение, в итоге тела будут двигать более плавно, но условие соединения может немного нарушаться.

В случае если соединение не жесткое (т.е. stiff == false), можно использовать следующие свойства соединения:
  • frequency — частота колебаний. Дело в том, что если прикладывать силу действующую в сторону исправления ошибки соединения и пропорциональную этой ошибке, то получится нечто вроде пружины. В принципе, можно было бы задавать просто коэффициент жесткости этой «пружины», но тогда его пришлось бы подбирать исходя из масс тел. В nape же автор сделал так, что задается частота колебаний получившейся системы, а nape уже сам рассчитывает коэффициент на основании того, какие тела соединены, это позволяет более интуитивно понятным образом задавать параметры соединения.
  • damping — скорость затухания колебаний «пружины»
  • maxForce — максимальная сила, которую может приложить соединение. Учитывайте, что единицей длины в nape является один пиксель, а значит значения силы будут большие
  • breakUnderForce — если это значение равно true, то при превышении силы maxForce соединение будет отключено. Если свойство removeOnBreak == true, то соединение будет удалено из мира, иначе свойство active будет установлено в значение false.
  • maxError — максимальное значение ошибки, которое будет устраняться за одну секунду. Т.е. для AngleJoint например это ограничение на скорость вращения тел друг относительно друга: соединение не будет ускорять взаимное вращение тел быстрее заданного значения
  • breakUnderError — аналогично свойству breakUnderForce, но отключает соединение, если превышена ошибка maxError

DistanceJoint

DistanceJoint — во многом аналогичен pivotJoint: точно также задаются два тела и две точки на этих телах. Разница в том, что если для PivotJoint расстояние между этими точками всегда должно быть равно нулю, то для DistanceJoint это расстояние должно лежать в заданных пределах.
distJoint = new DistanceJoint(distJointBody1, distJointBody2, new Vec2, new Vec2, 10, 200);
distJoint.space = space;

В данном случае я установил, что расстояние между центрами тел будет от 10 до 200 пикселей.

LineJoint

LineJoint — чуть сложнее. На первом теле задается отрезок, а на втором — точка, и эта точка всегда должна лежать на отрезке. Отрезок задается как начальная точка луча, направление луча, а также минимальное и максимальное расстояния от начала луча.

lineJoint = new LineJoint(lineJointBody1, lineJointBody2, new Vec2(0, 0), new Vec2(0, -50), new Vec2(1, 0), -60, 60);
lineJoint.ignore = true;
lineJoint.space = space;


AngleJoint

AngleJoint задает такое ограничение на углы поворота тел: jointMin < rotation2 * ratio — rotation1 < jointMax.

В случае если ratio = 1 это просто ограничение на угол поворота одного тела относительно другого. При использовании вместе с PivotJoint дает простой и понятный результат: тела вращаются вокруг общей точки, но в определенных пределах.
space.constraints.add(angleJoint = new AngleJoint(pivotJointBody1, pivotJointBody2, 0, Math.PI / 2));


Если же ratio != 1, то получается что-то вроде шестеренок разных размеров.
angleJointWithRatio = new AngleJoint(angleJointWithRatioBody1, angleJointWithRatioBody2);
angleJointWithRatio.ratio = 2;
angleJointWithRatio.space = space;

Теперь одно из этих тел всегда будет вращаться быстрее другого в два раза, точнее будет повернуто на в два раза больший угол.

MotorJoint

MotorJoint задает ограничение на скорости вращения двух тел angularVel2 * ratio — angularVel1 = rate.

Опять же в случае ratio = 1 все просто — это обычный мотор, т.е. просто задаем разность между скоростями вращения двух тел и к телам прикладывается момент вращающий их друг относительно друга. Имеет смысл еще сразу добавить PivotJoint, чтобы скрепить тела вместе. Получиться что-то вроде машинки.
motorJoint = new MotorJoint(motorJointBody1, motorJointBody2, Math.PI * 2, 1);
motorJoint.ignore = true;
motorJoint.space = space;
space.constraints.add(new PivotJoint(motorJointBody1, motorJointBody2, new Vec2, new Vec2));


Вариант с ratio != 1 аналогично AngleJoint даст эффект шестеренок разного размера, но теперь они еще и будут вращаться относительно друг друга.

Вообще говоря, у меня есть ощущение если ratio != 1, то нарушается закон сохранения момента импульса, это же касается и AngleJoint. Впрочем если прикрепить оба тела PivotJoint'ами к земле, то всё ок. Иначе — будет физическая неправда. Рассмотрим такую систему: тело1, к нему прикреплены шестерня1 и шестерня2. Чувак сидит на шестерне1 и схватившись за шестерню2 вращает их относительно друг друга. Шестерни вращаются друг относительно друга и их скорости соотносятся как радиусы шестерней. В силу закона сохранения импульса тело1 должно начать вращаться так, чтобы суммарный момент импульса не изменился. Если обе шестерни прикреплены к телу с бесконечным моментом инерции (к Земле), то ей достаточно начать вращаться с бесконечно малой скоростью, т.е. практически можно ничего и не делать, но в остальных случаях (то есть если шестерни не прикреплены к общему телу, либо это тело летает само по себе) есть некая нефизичность происходящего.

Пример
package Tests
{
        import com.bit101.components.*;
        import flash.events.Event;
        import flash.geom.Rectangle;
        import nape.constraint.AngleJoint;
        import nape.constraint.DistanceJoint;
        import nape.constraint.LineJoint;
        import nape.constraint.MotorJoint;
        import nape.constraint.PivotJoint;
        import nape.dynamics.*;
        import nape.geom.*;
        import nape.phys.*;
        import nape.shape.*;
        import nape.space.*;
        import nape.util.*;
        
        public class Test2 extends Test
        {
                private var body1:Body;
                private var joint1:PivotJoint;
                
                private var active:Label;
                private var stiff:CheckBox;
                private var frequency:HUISlider;
                private var damping:HUISlider;
                private var maxForceOn:CheckBox;
                private var maxForce:HUISlider;
                private var breakUnderForce:CheckBox;
                private var maxErrorOn:CheckBox;
                private var maxError:HUISlider;
                private var breakUnderError:CheckBox;
                
                private var angleJointEnabled:CheckBox;
                private var angleJointLimits:RangeSlider;
                
                private var pivotJointBody1:Body;
                private var pivotJointBody2:Body;
                private var pivotJoint:PivotJoint;
                
                private var distJointBody1:Body;
                private var distJointBody2:Body;
                private var distJoint:DistanceJoint;
                
                private var lineJointBody1:Body;
                private var lineJointBody2:Body;
                private var lineJoint:LineJoint;
                
                private var angleJoint:AngleJoint;
                
                private var angleJointWithRatioBody1:Body;
                private var angleJointWithRatioBody2:Body;
                private var angleJointWithRatio:AngleJoint;
                
                private var motorJointBody1:Body;
                private var motorJointBody2:Body;
                private var motorJoint:MotorJoint;
                
                public function Test2() 
                {
                        super("Test #2. Constraints.");
                        
                        createWalls(Material.wood());
                        
                        var filter:InteractionFilter = new InteractionFilter(0x0);

                        body1 = new Body(BodyType.DYNAMIC);
                        body1.shapes.add(new Polygon(Polygon.rect(400, 100, 100, 160), Material.steel(), filter));
                        body1.align();
                        body1.space = space;
                        
                        var joint1Pos:Vec2 = new Vec2(0, -50); 
                        space.constraints.add(joint1 = new PivotJoint(space.world, body1, body1.localToWorld(joint1Pos), joint1Pos));
                        joint1.removeOnBreak = false;
                        
                        filter = new InteractionFilter(0x10, 0x11);
                        
                        pivotJointBody1 = new Body(BodyType.DYNAMIC);
                        pivotJointBody1.shapes.add(new Polygon(Polygon.rect(100, 100, 100, 160), Material.steel(), filter));
                        pivotJointBody1.align();
                        pivotJointBody1.space = space;
                        
                        pivotJointBody2 = new Body(BodyType.DYNAMIC);
                        pivotJointBody2.shapes.add(new Polygon(Polygon.rect(100, 100, 160, 100), Material.steel(), filter));
                        pivotJointBody2.align();
                        pivotJointBody2.space = space;
                        
                        var pivotJointWorldPoint:Vec2 = new Vec2(150, 150);
                        pivotJoint = new PivotJoint(pivotJointBody1, pivotJointBody2, pivotJointBody1.worldToLocal(pivotJointWorldPoint), pivotJointBody2.worldToLocal(pivotJointWorldPoint));
                        pivotJoint.ignore = true;
                        pivotJoint.stiff = false;
                        pivotJoint.space = space;
                        
                        space.constraints.add(angleJoint = new AngleJoint(pivotJointBody1, pivotJointBody2, 0, Math.PI / 2));
                        
                        filter = new InteractionFilter(0x100, 0x101);
                        
                        distJointBody1 = new Body(BodyType.DYNAMIC);
                        distJointBody1.shapes.add(new Circle(50,new Vec2(100, 300), Material.steel(), filter));
                        distJointBody1.align();
                        distJointBody1.space = space;
                        
                        distJointBody2 = new Body(BodyType.DYNAMIC);
                        distJointBody2.shapes.add(new Circle(50,new Vec2(200, 300), Material.steel(), filter));
                        distJointBody2.align();
                        distJointBody2.space = space;
                        
                        var distance:Number = distJointBody1.position.sub(distJointBody2.position).length;
                        distJoint = new DistanceJoint(distJointBody1, distJointBody2, new Vec2, new Vec2, distance / 2, distance * 2);
                        distJoint.ignore = true;
                        distJoint.space = space;
                        
                        filter = new InteractionFilter(0x1000, 0x1001);
                        
                        lineJointBody1 = new Body(BodyType.DYNAMIC);
                        lineJointBody1.shapes.add(new Polygon(Polygon.rect(300, 300, 160, 100), Material.steel(), filter));
                        lineJointBody1.align();
                        lineJointBody1.space = space;
                        
                        lineJointBody2 = new Body(BodyType.DYNAMIC);
                        lineJointBody2.shapes.add(new Polygon(Polygon.rect(300, 300, 100, 160), Material.steel(), filter));
                        lineJointBody2.align();
                        lineJointBody2.space = space;
                        
                        lineJoint = new LineJoint(lineJointBody1, lineJointBody2, new Vec2(0, 0), new Vec2(0, -50), new Vec2(1, 0), -60, 60);
                        lineJoint.ignore = true;
                        lineJoint.space = space;
                        
                        filter = new InteractionFilter(0);

                        angleJointWithRatioBody1 = new Body(BodyType.DYNAMIC);
                        angleJointWithRatioBody1.shapes.add(new Polygon(Polygon.rect(300, 300, 50, 70), Material.steel(), filter));
                        angleJointWithRatioBody1.align();
                        angleJointWithRatioBody1.space = space;
                        space.constraints.add(new PivotJoint(space.world, angleJointWithRatioBody1, angleJointWithRatioBody1.position, new Vec2));

                        angleJointWithRatioBody2 = new Body(BodyType.DYNAMIC);
                        angleJointWithRatioBody2.shapes.add(new Polygon(Polygon.rect(400, 300, 50, 70), Material.steel(), filter));
                        angleJointWithRatioBody2.align();
                        angleJointWithRatioBody2.space = space;
                        space.constraints.add(new PivotJoint(space.world, angleJointWithRatioBody2, angleJointWithRatioBody2.position, new Vec2));
                        
                        angleJointWithRatio = new AngleJoint(angleJointWithRatioBody1, angleJointWithRatioBody2);
                        angleJointWithRatio.ratio = 2;
                        angleJointWithRatio.space = space;
                        
                        filter = new InteractionFilter(0x10000, 0x10001);
                        
                        motorJointBody1 = new Body(BodyType.DYNAMIC);
                        motorJointBody1.shapes.add(new Polygon(Polygon.rect(100, 400, 100, 30), Material.steel(), filter));
                        motorJointBody1.align();
                        motorJointBody1.space = space;

                        motorJointBody2 = new Body(BodyType.DYNAMIC);
                        motorJointBody2.shapes.add(new Circle(25, new Vec2(150, 415), Material.steel(), filter));
                        motorJointBody2.align();
                        motorJointBody2.space = space;
                        
                        space.constraints.add(new PivotJoint(motorJointBody1, motorJointBody2, new Vec2, new Vec2));
                        
                        motorJoint = new MotorJoint(motorJointBody1, motorJointBody2, Math.PI * 2, 1);
                        motorJoint.ignore = true;
                        motorJoint.space = space;
                        
                }
                
                override public function createUI(parent:VBox):void
                {
                        new Label(parent, 0, 0, "Pivot joint in the upper right corner: ");
                        new PushButton(parent, 0, 0, "Restore", function(e:Event):void { joint1.active = true; } );
                        active = new Label(parent);
                        stiff = new CheckBox(parent, 0, 0, "Stiff");
                        stiff.selected = joint1.stiff;
                        frequency = new HUISlider(parent, 0, 0, "Frequency");
                        frequency.minimum = 0.5;
                        frequency.maximum = 50;
                        frequency.value = joint1.frequency;
                        damping = new HUISlider(parent, 0, 0, "Damping");
                        damping.minimum = 0;
                        damping.maximum = 1;
                        damping.value = joint1.damping;
                        maxForceOn = new CheckBox(parent, 0, 0, "Use max force");
                        maxForceOn.selected = false;
                        maxForce = new HUISlider(parent, 0, 0, "Max force 10^");
                        maxForce.minimum = 5;
                        maxForce.maximum = 8;
                        maxForce.value = 7;
                        breakUnderForce = new CheckBox(parent, 0, 0, "Break under force");
                        breakUnderForce.selected = joint1.breakUnderForce;
                        maxErrorOn = new CheckBox(parent, 0, 0, "Use max error");
                        maxErrorOn.selected = false;
                        maxError = new HUISlider(parent, 0, 0, "Max error");
                        maxError.minimum = 0;
                        maxError.maximum = 100;
                        maxError.value = 100;
                        breakUnderError = new CheckBox(parent, 0, 0, "Break under error");
                        breakUnderError.selected = joint1.breakUnderError;
                        new Label(parent, 0, 0, "Angle joint between two boxes:");
                        angleJointEnabled = new CheckBox(parent, 0, 0, "Active");
                        angleJointEnabled.selected = angleJoint.active;
                        var hbox:HBox = new HBox(parent);
                        new Label(hbox, 0, 0, "Limits");
                        angleJointLimits = new HRangeSlider(hbox);
                        angleJointLimits.minimum = -360;
                        angleJointLimits.maximum = 360;
                        angleJointLimits.lowValue = angleJoint.jointMin * 180 / Math.PI;
                        angleJointLimits.highValue = angleJoint.jointMax * 180 / Math.PI;
                }
                
                override public function update():void
                {
                        active.text = joint1.active ? "Joint is active" : "Joint is not active. Push Restore to activate. (Try disabling breakUnderError and breakUnderForce before activating)";
                        joint1.stiff = stiff.selected;
                        joint1.frequency = frequency.value;
                        joint1.damping = damping.value;
                        if (maxForceOn.selected)
                                joint1.maxForce = Math.pow(10, maxForce.value);
                        else
                                joint1.maxForce = Number.MAX_VALUE;
                        joint1.breakUnderForce = breakUnderForce.selected;
                        if (maxErrorOn.selected)
                                joint1.maxError = maxError.value;
                        else
                                joint1.maxError = Number.MAX_VALUE;
                        joint1.breakUnderError = breakUnderError.selected;
                        
                        angleJoint.active = angleJointEnabled.selected;
                        angleJoint.jointMin = angleJointLimits.lowValue * Math.PI / 180;
                        angleJoint.jointMax = angleJointLimits.highValue * Math.PI / 180;
                }
        }
}


Результат: (Найдите с помощью кнопок Next и Prev тест с названием Test 2.)
  • +24

Комментарии (7)

0
Спасибо за топик!
ещё спросить хотел: в старом nape, в space лежало неподвижное тело: space.STATIC
к нему можно было цеплять джоинты, например для ограничения углов вращения.
В новом такого тела нет, нужно создавать своё?
+1
Я написал, да видать слишком незаметно: есть такое, называется world.
0
скорее всего это я читаю невнимательно =)
Спасибо)
0
Спасибо за очередной пост. Однозначно плюсую.
0
спасибо за посты. Эх, попробовать что ли сваять чтонить на напе…
0
Отличный цикл статей.
Небольшой вопрос, как удалять физические тела в nape.
+1
точно писал где-то. как хочешь из всяких вариантов:
body.space = null
space.bodies.remove(body)
space.dynamics.remove(body)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.