
Новый 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 и общие для всех соединений свойства
PivotJoint — самое простое соединение, и, наверное, самое востребованное. Тела соединяются так, как будто их свинтили винтом, и они теперь вокруг этого винта крутятся. Для создания PivotJoint нужно указать два тела и точки на этих телах, которые должны всегда совпадать.
В качестве параметра конструктора передаются точки в локальных координатах тела. Но во-первых после вызова align мы точно не знаем, какие у нас локальные координаты, а во-вторых обычно надо добавить соединение тела, условие которого уже выполняется, поэтому я задал точку в глобальных координатах и воспользовался функцией worldToLocal для перевода.
Если нужно привязать тело к «земле», то в качестве второго тела можно использовать любое статическое тело. В классе Space есть свойство world, которое является таким заранее созданным статическим телом.
Здесь я наоборот задал локальные координаты в теле body1. Локальная система координат тела world совпадает с глобальной.
У всех ограничений есть интересное свойство ignore:
Оно делает так, что столкновения между соединенными телами не учитываются. Часто это бывает полезно.
Иногда бывает нужно временно отключить соединение. Можно удалить соединение из мира, а можно просто установить свойство active в значение false.
Кроме того все типы соединений имеют свойство stiff. На русский это можно перевести как «жесткое соединение».
Разница в поведении такова: если соединение жесткое, то каждый шаг nape будет устранять ошибку соединения (т.е. разницу между текущим положением тел и положением тел, при котором условие выполняется) перемещая тела, если же соединение не жесткое, то к телам будут прикладываться силы необходимые для приведения тел в нужное положение, в итоге тела будут двигать более плавно, но условие соединения может немного нарушаться.
В случае если соединение не жесткое (т.е. stiff == false), можно использовать следующие свойства соединения:
DistanceJoint
DistanceJoint — во многом аналогичен pivotJoint: точно также задаются два тела и две точки на этих телах. Разница в том, что если для PivotJoint расстояние между этими точками всегда должно быть равно нулю, то для DistanceJoint это расстояние должно лежать в заданных пределах.
В данном случае я установил, что расстояние между центрами тел будет от 10 до 200 пикселей.
LineJoint
LineJoint — чуть сложнее. На первом теле задается отрезок, а на втором — точка, и эта точка всегда должна лежать на отрезке. Отрезок задается как начальная точка луча, направление луча, а также минимальное и максимальное расстояния от начала луча.
AngleJoint
AngleJoint задает такое ограничение на углы поворота тел: jointMin < rotation2 * ratio — rotation1 < jointMax.
В случае если ratio = 1 это просто ограничение на угол поворота одного тела относительно другого. При использовании вместе с PivotJoint дает простой и понятный результат: тела вращаются вокруг общей точки, но в определенных пределах.
Если же ratio != 1, то получается что-то вроде шестеренок разных размеров.
Теперь одно из этих тел всегда будет вращаться быстрее другого в два раза, точнее будет повернуто на в два раза больший угол.
MotorJoint
MotorJoint задает ограничение на скорости вращения двух тел angularVel2 * ratio — angularVel1 = rate.
Опять же в случае ratio = 1 все просто — это обычный мотор, т.е. просто задаем разность между скоростями вращения двух тел и к телам прикладывается момент вращающий их друг относительно друга. Имеет смысл еще сразу добавить PivotJoint, чтобы скрепить тела вместе. Получиться что-то вроде машинки.
Вариант с ratio != 1 аналогично AngleJoint даст эффект шестеренок разного размера, но теперь они еще и будут вращаться относительно друг друга.
Вообще говоря, у меня есть ощущение если ratio != 1, то нарушается закон сохранения момента импульса, это же касается и AngleJoint. Впрочем если прикрепить оба тела PivotJoint'ами к земле, то всё ок. Иначе — будет физическая неправда. Рассмотрим такую систему: тело1, к нему прикреплены шестерня1 и шестерня2. Чувак сидит на шестерне1 и схватившись за шестерню2 вращает их относительно друг друга. Шестерни вращаются друг относительно друга и их скорости соотносятся как радиусы шестерней. В силу закона сохранения импульса тело1 должно начать вращаться так, чтобы суммарный момент импульса не изменился. Если обе шестерни прикреплены к телу с бесконечным моментом инерции (к Земле), то ей достаточно начать вращаться с бесконечно малой скоростью, т.е. практически можно ничего и не делать, но в остальных случаях (то есть если шестерни не прикреплены к общему телу, либо это тело летает само по себе) есть некая нефизичность происходящего.
Пример
Результат: (Найдите с помощью кнопок Next и Prev тест с названием Test 2.)
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
- romamik
Комментарии (7)
ещё спросить хотел: в старом nape, в space лежало неподвижное тело: space.STATIC
к нему можно было цеплять джоинты, например для ограничения углов вращения.
В новом такого тела нет, нужно создавать своё?
Спасибо)
Небольшой вопрос, как удалять физические тела в nape.
body.space = null
space.bodies.remove(body)
space.dynamics.remove(body)