Новый 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)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.